From 13fdbb48a02075686353e43ec1985781ccc5c18d Mon Sep 17 00:00:00 2001 From: Peter Weinberg Date: Sat, 16 Dec 2017 03:11:30 -0500 Subject: [PATCH] =?UTF-8?q?major=20application=20refactor,=20upgrade=20to?= =?UTF-8?q?=200.2.0=20=F0=9F=8E=89,=20changes=20include:=20=20=20*=20add?= =?UTF-8?q?=20test=20framework=20and=20tests=20for=20all=20challenges=20?= =?UTF-8?q?=20=20=20=20=20=20*=20add=20test=20hooks:=20beforeAll,=20before?= =?UTF-8?q?Each,=20afterEach,=20afterAll=20=20=20=20=20=20=20*=20add=20abi?= =?UTF-8?q?lity=20to=20utilize=20assert.xxx=20methods=20=20=20=20=20=20=20?= =?UTF-8?q?*=20add=20disable=20tests=20feature=20=20=20=20=20=20=20*=20add?= =?UTF-8?q?=20test=20report=20feature=20=20=20=20=20=20=20*=20use=20Jest?= =?UTF-8?q?=20to=20test=20challenge=20solutions=20=20=20*=20fix=20serious?= =?UTF-8?q?=20codeStore=20entry=20duplication=20bug=20=20=20*=20improve=20?= =?UTF-8?q?styles,=20remove/refactor=20app=20event=20listeners=20=20=20*?= =?UTF-8?q?=20modify=20simple=20drag,=20track=20pane=20state=20with=20redu?= =?UTF-8?q?x=20=20=20*=20double=20click=20divider=20to=20snap=20to=20edge?= =?UTF-8?q?=20=20=20*=20editor=20features:=20=20=20=20=20=20=20*=20disable?= =?UTF-8?q?=20laxbreak,=20asi=20linter=20rules=20=20=20=20=20=20=20*=20lin?= =?UTF-8?q?t=20warnings=20in=20gutter=20=20=20=20=20=20=20*=20highlight=20?= =?UTF-8?q?all=20selection=20matches=20=20=20=20=20=20=20*=20autocomplete,?= =?UTF-8?q?=20hint=20dialog=20=20=20*=20add=20new=20shortcut=20keys,=20ena?= =?UTF-8?q?ble=20mac=20meta=20key=20(toggle=20solution,=20focus=20editor,?= =?UTF-8?q?=20autocomplete)=20=20=20*=20fix=20resetCode()=20typo=20in=20We?= =?UTF-8?q?lcome.js=20=3D>=20resetState()=20=20=20*=20improve=20resetState?= =?UTF-8?q?()=20logic,=20add=20timeout=20to=20prevent=20accidental=20reset?= =?UTF-8?q?s=20=20=20*=20prevent=20highlighting=20of=20editor=20contents?= =?UTF-8?q?=20when=20dragging=20divider=20=20=20*=20fixes=20/=20improvemen?= =?UTF-8?q?ts=20to=20various=20data=20structures=20=20=20*=20major=20impro?= =?UTF-8?q?vements=20to=20Graph=20data=20structure=20=20=20*=20import=20/?= =?UTF-8?q?=20export=20all=20types=20as=20module=20=20=20*=20improve=20mod?= =?UTF-8?q?al=20extensibility=20=20=20*=20unmount=20closed=20modal=20=20?= =?UTF-8?q?=20*=20remove=20semicolons=20=20=20*=20update=20README.md=20=20?= =?UTF-8?q?=20*=20use=20lodash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc | 3 + CONTRIBUTING.md | 145 ++++- README.md | 12 +- RESOURCES.md | 18 + package.json | 6 +- src/App.js | 170 +++-- src/App.test.js | 8 - src/__tests__/AlgorithmChallenges.test.js | 19 + src/__tests__/DataStructures.test.js | 21 + src/__tests__/SortingAlgorithms.test.js | 19 + src/actions/console.js | 55 +- src/actions/editor.js | 66 +- src/actions/modal.js | 57 +- src/actions/panes.js | 60 ++ src/actions/resources.js | 8 - src/actions/types.js | 22 + src/assets/codeRef.js | 63 +- .../seed/algorithms/AnagramPalindrome.js | 38 +- src/assets/seed/algorithms/BubbleSort.js | 23 +- src/assets/seed/algorithms/BucketSort.js | 34 +- .../seed/algorithms/GenerateCheckerboard.js | 26 +- src/assets/seed/algorithms/HeapSort.js | 99 ++- src/assets/seed/algorithms/InsertionSort.js | 21 +- src/assets/seed/algorithms/Mergesort.js | 29 +- .../seed/algorithms/NoTwoConsecutiveChars.js | 60 +- src/assets/seed/algorithms/Quicksort.js | 37 +- src/assets/seed/algorithms/SelectionSort.js | 33 +- .../seed/algorithms/SortingBenchmarks.js | 239 +++---- src/assets/seed/algorithms/SumAllPrimes.js | 31 +- .../seed/data-structures/BinarySearchTree.js | 585 +++++++++--------- .../seed/data-structures/DoublyLinkedList.js | 391 ++++++------ src/assets/seed/data-structures/Graph.js | 454 ++++++++++++-- src/assets/seed/data-structures/HashTable.js | 155 +++-- src/assets/seed/data-structures/LinkedList.js | 249 ++++---- src/assets/seed/data-structures/MaxHeap.js | 114 ++-- .../seed/data-structures/PriorityQueue.js | 227 ++++--- src/assets/seed/data-structures/Queue.js | 160 ++--- src/assets/seed/data-structures/Stack.js | 101 +-- src/assets/seed/welcome.js | 14 +- src/assets/testRef.js | 48 ++ .../tests/algorithms/AnagramPalindrome.js | 18 + src/assets/tests/algorithms/BubbleSort.js | 23 + src/assets/tests/algorithms/BucketSort.js | 29 + .../tests/algorithms/GenerateCheckerboard.js | 59 ++ src/assets/tests/algorithms/HeapSort.js | 82 +++ src/assets/tests/algorithms/InsertionSort.js | 23 + src/assets/tests/algorithms/Mergesort.js | 23 + .../tests/algorithms/NoTwoConsecutiveChars.js | 63 ++ src/assets/tests/algorithms/Quicksort.js | 23 + src/assets/tests/algorithms/SelectionSort.js | 23 + src/assets/tests/algorithms/SumAllPrimes.js | 26 + .../tests/data-structures/BinarySearchTree.js | 376 +++++++++++ .../tests/data-structures/DoublyLinkedList.js | 402 ++++++++++++ src/assets/tests/data-structures/Graph.js | 381 ++++++++++++ src/assets/tests/data-structures/HashTable.js | 167 +++++ .../tests/data-structures/LinkedList.js | 306 +++++++++ src/assets/tests/data-structures/MaxHeap.js | 109 ++++ .../tests/data-structures/PriorityQueue.js | 268 ++++++++ src/assets/tests/data-structures/Queue.js | 140 +++++ src/assets/tests/data-structures/Stack.js | 157 +++++ src/components/CodeMirrorRenderer.js | 91 +-- src/components/Controls.js | 132 ++-- src/components/sidebar/Console.js | 123 ++++ src/components/sidebar/ConsoleOutput.js | 113 ---- src/components/sidebar/Menu.js | 33 +- src/components/sidebar/MenuMap.js | 81 +-- src/components/utils/Divider.js | 18 +- src/components/utils/ErrorBoundary.js | 14 +- src/components/utils/Fader.js | 15 +- src/components/utils/Modal.js | 104 ++-- src/components/utils/Pane.js | 33 + src/index.js | 55 +- src/reducers/console.js | 14 +- src/reducers/editor.js | 199 +++--- src/reducers/modal.js | 51 +- src/reducers/panes.js | 71 +++ src/reducers/resources.js | 20 - src/reducers/rootReducer.js | 18 +- src/reducers/utils.js | 102 +++ src/styles/app.css | 11 +- src/styles/codemirror.css | 4 + src/styles/console.css | 27 +- src/styles/index.css | 1 + src/styles/menu.css | 3 +- src/styles/modal.css | 11 +- src/utils/base64.js | 10 +- src/utils/editorConfig.js | 33 +- src/utils/regexp.js | 5 + src/utils/registerServiceWorker.js | 48 +- src/utils/resize.js | 69 --- src/utils/simpleDrag.js | 64 +- src/utils/styleListeners.js | 29 - src/utils/test/app/create-jest-test.js | 18 + src/utils/test/app/jest-test-scripts.js | 53 ++ src/utils/test/app/jest-test-utils.js | 42 ++ .../test/challenge/eval-code-run-tests.js | 36 ++ src/utils/test/challenge/execute-tests.js | 73 +++ src/utils/test/challenge/testReport.js | 12 + src/utils/test/common/is-test-disabled.js | 9 + yarn.lock | 91 +-- 100 files changed, 6328 insertions(+), 2226 deletions(-) create mode 100644 .eslintrc delete mode 100644 src/App.test.js create mode 100644 src/__tests__/AlgorithmChallenges.test.js create mode 100644 src/__tests__/DataStructures.test.js create mode 100644 src/__tests__/SortingAlgorithms.test.js create mode 100644 src/actions/panes.js delete mode 100644 src/actions/resources.js create mode 100644 src/actions/types.js create mode 100644 src/assets/testRef.js create mode 100644 src/assets/tests/algorithms/AnagramPalindrome.js create mode 100644 src/assets/tests/algorithms/BubbleSort.js create mode 100644 src/assets/tests/algorithms/BucketSort.js create mode 100644 src/assets/tests/algorithms/GenerateCheckerboard.js create mode 100644 src/assets/tests/algorithms/HeapSort.js create mode 100644 src/assets/tests/algorithms/InsertionSort.js create mode 100644 src/assets/tests/algorithms/Mergesort.js create mode 100644 src/assets/tests/algorithms/NoTwoConsecutiveChars.js create mode 100644 src/assets/tests/algorithms/Quicksort.js create mode 100644 src/assets/tests/algorithms/SelectionSort.js create mode 100644 src/assets/tests/algorithms/SumAllPrimes.js create mode 100644 src/assets/tests/data-structures/BinarySearchTree.js create mode 100644 src/assets/tests/data-structures/DoublyLinkedList.js create mode 100644 src/assets/tests/data-structures/Graph.js create mode 100644 src/assets/tests/data-structures/HashTable.js create mode 100644 src/assets/tests/data-structures/LinkedList.js create mode 100644 src/assets/tests/data-structures/MaxHeap.js create mode 100644 src/assets/tests/data-structures/PriorityQueue.js create mode 100644 src/assets/tests/data-structures/Queue.js create mode 100644 src/assets/tests/data-structures/Stack.js create mode 100644 src/components/sidebar/Console.js delete mode 100644 src/components/sidebar/ConsoleOutput.js create mode 100644 src/components/utils/Pane.js create mode 100644 src/reducers/panes.js delete mode 100644 src/reducers/resources.js create mode 100644 src/reducers/utils.js create mode 100644 src/utils/regexp.js delete mode 100644 src/utils/resize.js delete mode 100644 src/utils/styleListeners.js create mode 100644 src/utils/test/app/create-jest-test.js create mode 100644 src/utils/test/app/jest-test-scripts.js create mode 100644 src/utils/test/app/jest-test-utils.js create mode 100644 src/utils/test/challenge/eval-code-run-tests.js create mode 100644 src/utils/test/challenge/execute-tests.js create mode 100644 src/utils/test/challenge/testReport.js create mode 100644 src/utils/test/common/is-test-disabled.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..5e603ec --- /dev/null +++ b/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": "react-app" +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b57faec..a77ac1c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,16 +1,22 @@ -# Contributing Guidelines +# Contribution Guide **NOTE:** This app is built with React and Redux, but if you're just looking to add some new content (like a new algorithm or data structure), knowledge of React or Redux _**is not**_ a prerequisite! You need only know a little JavaScript, and, of course, the topic you are looking to add. -### To Install/Run: +### Contents +- [How To Install / Run](#how-to-install--run) +- [Adding Challenges / Topics](#adding-challenges--topics) +- [Adding Tests](#adding-tests) +- [Modifying Challenges / Topics](#modifying-challenges--topics) + +### How To Install / Run - Be sure that you have NodeJS installed - Fork repo, clone locally, and run `npm install` or `yarn install` - In the root directory, run `npm start` or `yarn start` -### Adding Challenges / Topics: +### Adding Challenges / Topics To add a new algorithm or data structure, here's what you'll need: -- Some seed, or default code (usually a function or class declaration). This code should not be a complete solution, but it should be able to run without breaking. -- A working (and ideally good) solution to the problem you are introducing. +- Some seed, or default code (usually a function or class declaration). This code should not be a complete solution, but it should be able to run without breaking. +- A working (and ideally good) solution to the problem you are introducing. - At least one resource (this could be an article, a youtube video, a link to an interactive challenge that covers the problem, an image, etc.). If your problem does not have any viable associated resources, think of something related as a placeholder (see the Generate Checkerboard Challenge). To actually add it to the app, you only need to follow a few simple steps: @@ -21,13 +27,13 @@ __Add Seed file:__ ```js export default { title: 'Super Difficult Sorting Algo', - seed: + seed: `/** * @function superDifficultSortingAlgo * @param {number[]} arr - * @return {number[]} + * @returns {number[]} */ - + function superDifficultSortingAlgo(arr) { return arr; } @@ -36,9 +42,9 @@ function superDifficultSortingAlgo(arr) { `/** * @function superDifficultSortingAlgo * @param {number[]} arr - * @return {number[]} + * @returns {number[]} */ - + function superDifficultSortingAlgo(arr) { // perform super difficult sort here // this is where the solution goes! @@ -54,14 +60,119 @@ function superDifficultSortingAlgo(arr) { - Note the backticks, and that code inside the template literal strings _must_ start backed all the way up against the gutter to achieve proper formatting in the editor). __Import seed:__ -- Once you have a complete seed file, according to the above format, simply import it into `src/assets/seed/codeRef.js` and add it to the appropriate array. +- Once you have a complete seed file, according to the above format, simply import it into `src/assets/seed/codeRef.js` and add it to the appropriate array. - The order that it appears in the array, is the order that it will appear in the UI, so keep this in mind! -- That's it! Once the seed file is imported and added to the array, the rest will happen automatically. +- That's it! Once the seed file is imported and added to the array, the rest will happen automatically. __Update other files:__ -- Once you've confirmed your new challenge is working correctly, be sure to update both the contents section of [README.md](https://github.com/no-stack-dub-sack/cs-playground-react/blob/master/README.md) and add your challenge's resources to [RESOURCES.md](https://github.com/no-stack-dub-sack/cs-playground-react/blob/master/RESOURCES.md). +- Once you've confirmed your new challenge is working correctly, be sure to update both the "Contents" section of [README.md](https://github.com/no-stack-dub-sack/cs-playground-react/blob/master/README.md) and add your challenge's resources to [RESOURCES.md](https://github.com/no-stack-dub-sack/cs-playground-react/blob/master/RESOURCES.md). + +### Adding Tests +Once you've added your new challenge and confirmed that everything works, please add some tests (*__Note:__* If adding tests seems too complicated, and you'd like to cover a good topic that's not already covered, don't hesitate to contribute anyway! I'd hate for the extra complexity to scare away any would-be contributors. Challenges will work without tests, however it is preferred that they are included with all new challenges.). + + If you are adding an algorithm challenge, add enough tests to make reasonably sure the user's solution is valid, and if you're adding a data structure, add tests for the key methods that are essential to understanding the fundamentals of the topic you're adding. Any extra tests that go beyond fundamental understanding can be disabled by default. + + The goal of the tests is to allow users to verify that their understanding of the fundamental concepts are correct. Tests/results will be displayed in the mock console. + +__Create a test seed file:__ +- The test framework parses the user's code as well as our tests from strings. The seed file _must_ contain a `tests` export, and _may_ contain a `tail` export. The `tests` export is an array of tests, and the `tail` export is a string containing any code that you want to run after the user's code and before the tests execute. For more complex tests, this is where we can initialize variables shared across tests, add hidden methods to classes, or run functions like `beforeEach`. +- Please see files in the `src/assets/tests/` folder for examples of how to set this up. + +__Import your test seed:__ +- To get your tests running in the app, find the `src/assets/seed/testRef.js` file, import your test file as a module, e.g. `import * as DataStructure from './tests/data-structures/DataStructure'`, and add it to the object being exported from the file. +- This will help you test your code as you go, but for even faster feedback, you can, and should, add your tests to the appropriate file in the `src/__tests__` directory. Just find the file that corresponds to the challenge you're adding, and add the title of the challenge (no spaces, preserve caps) to the `IDS` array. +- Once this is done, you can run `yarn run test` (or `npm run test`), and Jest will run in watch mode, re-running the tests each time you make a change. This way you can easily track your progress and make sure your tests are working. Regardless of the results of `yarn run test`, __ALWAYS__ test your code in the UI before making a PR. + +__Add your tail code (optional):__ +- If you'd like to execute any code before the tests run, or make code globally available to all tests, here's where you'll define it. There's many examples of this the the above mentioned directory. Most simple algorithm challenges will not need a tail, but complex data structure tests likely will. +- Most importantly with the tail, is the ability to add test hooks, similar to Mocha, Jest, or other frameworks. In the tail, you can add an object called `testHooks` which may have the following methods: `beforeAll`, `beforeEach`, `afterEach`, and `afterAll`. As the names imply, these functions will run before or after each or all tests. +- For example, if you are writing tests for a data structure, you can use the same test structure for all the tests, but it will likely need to be initialized before every one, and maybe have some test data added to it. Before the `testHooks` object, you could add a hidden __clear__ method to the class's prototype. + +__Add your tests:__ +- Once your tail is set up (if you need one), you can define your tests. +- Each test is an object with an `expression` and `message` key, and optional `method` and `expected` keys. +- The tests use Node's simple assertion library. For tests to pass, the expression should evaluate to true. +- Your expression can be a simple one line expression, or a more complex function defined as an Immediately Invoked Function Expression (IIFE), as long as it can evaluate to true under the right conditions. +- By default, the tests use `assert(expression, message)`, however, if you'd like to use a particular method of `assert` such as `deepEqual` or `strictEqaul`, you can simply define a `method` key on your test with the method name as key. Now, you must also define an `expected` key, so the test knows what to compare with your expression, i.e. `assert.deepEqual(expression, expected, message)`. +- Be sure that your test messages are as clear and concise as possible, and descriptive enough to give the user a clear idea of what the test is asking. + +__Test formatting:__ +- Also note that test messages should be formatted correctly. Keywords, numbers, variables or anything else that represents actual code should be wrapped in `` tags. While methods that take arguments should have type annotations in the JSDoc style. + +__Disabling Tests:__ +- For data structures that are very complex, some methods you might add go beyond a fundamental understanding of the concepts. For example, a `pathFromTo` method for a Graph is cool, but might be hard for some users, and is not crucial to learning the basic concepts of a Graph. So this might be a good method to have disabled by default. To disable a test by default, you can add the following code to your test's expression: + +```js +if (isTestDisabled(DataStructure, 'method')) { + return 'DISABLED' +} +``` + +This code will check for the presence of the method as defined by the second argument. If that method is not defined on the class, the test will be disabled by default. Once the method is defined, the test will begin working. + +Here's a simplified example of what all of this might look like put together: + +```js +export const tail = ` +if (typeof new DataStructure() === 'object') { + DataStructure.prototype.__clear__ = function() { + this.__data__ = null + return true + } +} + +let __dataStructure__ +const testHooks = { + beforeAll: () => { + __dataStructure__ = new DataStructure() + }, + beforeEach: () => { + // clear data structure + __dataStructure__.__clear__() + // then add test data + typeof __dataStructure__.add === 'function' && + [3, 2, 4, 1].forEach(n => __dataStructure__.add(n)) + }, + afterEach: () => { + // runs after each test + }, + afterAll: () => { + __dataStructure__ = null + } +}` + +export const tests = [ + // tests + { + expression: `typeof __dataStructure__.remove === 'function'`, + message: `The data structure has an add method: @param {(number|string)} value` + }, + // more tests + { + expression: `(() => { + if (__dataStructure__.remove() !== 3) { + return false + } + + return true + })()`, + message: `The remove method removes elements` + }, + // more tests + { + method: 'deepEqual', + expression: `(() => { + if (isTestDisabled(DataStructure, 'sort')) { + return 'DISABLED' + } + return __dataStructure__.sort() + }`, + expect: [1, 2, 3, 4], + message: `The sort method returns a sorted array` + }, +] +``` -### Modifying Challenges / Topics: -- Simply find the seed file in the appropriate `src/assets/seed/` directory and modify the seed or solution there. -- To see your updates in the browser, you will need to reset the app's state, since state is saved in local storage. Type `resetState()` anywhere in the editor and hit the "Run Code" button twice (the first time a warning that you are about to reset the app will be logged to the console). After resetting, your changes should show up. - +### Modifying Challenges / Topics +- Simply find the seed file in the appropriate `src/assets/seed/` directory and modify the seed or solution there. +- To see your updates in the browser, you will need to reset the app's state, since state is saved in local storage. Type `resetState()` anywhere in the editor and hit the "Run Code" button twice (the first time a warning that you are about to reset the app will be logged to the console). After resetting, your changes should show up. diff --git a/README.md b/README.md index 4b2b619..1bc072f 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,16 @@ The app is currently live here: https://cs-playground-react.surge.sh/ ``` - The editor has SublimeText keybindings. - Shortcut keys: - - Go to the next challenge: CTRL + SHIFT + > - - Go to the previous challenge: CTRL + SHIFT + < - - Run code: CTRL + SHIFT + ENTER + - Go to the next challenge: CMD/CTRL + SHIFT + . + - Go to the previous challenge: CMD/CTRL + SHIFT + , + - Jump to solution/seed: CMD/CTRL + SHIFT + S + - Run code: CMD/CTRL + SHIFT + ENTER + - Clear Console: ALT + SHIFT + DELTE/BACKSPACE + - Open autocomplete dropdown: CTRL + SPACE + - Focus Editor: CMD/CTRL + SPACE +### NOTE: +The [JSDoc](https://github.com/jsdoc3/jsdoc)-like documentation found throughout the editor's files are just that: JSDoc-_like_. These comments would not produce proper documentation if the JSDoc utility was ran on these source files. Proper JSDoc would have meant overcrowding the code itself with comments, which, for the purposes of this project, I did not want to do. These comments, instead, loosely follow the JSDoc style, and are just meant as a recognizable reference for users, so that they can easily see how each function, parameter, class, property, and method is meant to be used. ## Contents: ### Sorting Algorithms: diff --git a/RESOURCES.md b/RESOURCES.md index 830318e..5b75f6a 100644 --- a/RESOURCES.md +++ b/RESOURCES.md @@ -1,3 +1,19 @@ +# General + +### Blogs, Code, Articles +Here's a great blog and compendium repo about computer science concepts in JavaScript, with posts covering many (if not all) of the concepts covered in this app (in both ES5 and ES6): +- https://github.com/benoitvallon/computer-science-in-javascript/tree/master/data-structures-in-javascript +- http://blog.benoitvallon.com/ + +### Interactive Exploration and Visualization +Here's a couple of _awesome_ resources for visualizing and interactively exploring the way that sorting algorithms and data structures work. Both sites have their strengths and weaknesses, and cover just about every topic imaginable between them. I think you'll find them both immensely helpful! +- https://visualgo.net/en +- https://www.cs.usfca.edu/~galles/visualization/Algorithms.html + +### Big-O Notation +Here's a pretty awesome Big-O notation cheat sheet to help you learn the ropes! Know thy complexity! +- http://bigocheatsheet.com/ + # Sorting Algorithms ### Quicksort @@ -117,8 +133,10 @@ ### Graphs http://www.geeksforgeeks.org/graph-and-its-representations/ http://www.geeksforgeeks.org/implementation-graph-javascript/ +http://blog.benoitvallon.com/data-structures-in-javascript/the-graph-data-structure/ https://en.wikipedia.org/wiki/Adjacency_list https://www.cs.usfca.edu/~galles/visualization/ConnectedComponent.html __(interactive visualization)__ +https://visualgo.net/en/dfsbfs __(SUPER interactive graph creation & traversal visualization)__ # Other Algorithms diff --git a/package.json b/package.json index d181f32..10b256e 100644 --- a/package.json +++ b/package.json @@ -6,11 +6,12 @@ "axios": "^0.17.1", "codemirror": "^5.31.0", "jshint": "^2.9.5", - "react": "^16.0.0", + "lodash": "^4.17.4", + "react": "16.2.0", "react-codemirror2": "^3.0.7", "react-dom": "^16.0.0", "react-redux": "^5.0.6", - "react-scripts": "1.0.15", + "react-scripts": "1.1.0", "react-transition-group": "^2.2.1", "redux": "^3.7.2", "shortid": "^2.2.8" @@ -24,6 +25,7 @@ "deploy": "yarn run build && cd build && surge --domain cs-playground-react.surge.sh" }, "devDependencies": { + "chalk": "^2.3.0", "prop-types": "^15.6.0", "redux-devtools-extension": "^2.13.2" } diff --git a/src/App.js b/src/App.js index 360c803..4e3033f 100644 --- a/src/App.js +++ b/src/App.js @@ -1,66 +1,138 @@ -import CodeMirrorRenderer from './components/CodeMirrorRenderer'; -import ConsoleOutput from './components/sidebar/ConsoleOutput'; -import Controls from './components/Controls'; -import Divider from './components/utils/Divider'; -import Menu from './components/sidebar/Menu'; -import Modal from './components/utils/Modal'; -import React, { Component } from 'react'; -import resizePanes from './utils/resize'; -import shortid from 'shortid'; -import axios from 'axios'; -import './styles/app.css'; +import CodeMirrorRenderer from './components/CodeMirrorRenderer' +import { connect } from 'react-redux' +import Console from './components/sidebar/Console' +import Controls from './components/Controls' +import Divider from './components/utils/Divider' +import { dragHorizontal, dragVertical, doubleClick } from './actions/panes' +import Menu from './components/sidebar/Menu' +import Modal from './components/utils/Modal' +import Pane from './components/utils/Pane' +import React, { Component } from 'react' +import { renderAnnouncementUtil } from './actions/modal' +import shortid from 'shortid' +import axios from 'axios' +import './styles/app.css' -// TODO: add clear console button -// TODO: make it so clearConsole() can be commented out +/** NEW FEATURES: + * Double click divider to hide contents or console pane + * Preserve Pane State + * Testing challenges + * Fix clearCode() typo! + * Improved various data structures + * Improved clearState() - timeout + * fixed codeStore bug + * prevent text highlighting on divider drag + * new shortcut keys + * Editor enhancements: + * focus shortcut + * Autocomplete + * linter suppressions + * gutter linter warnings + * match selection + */ + +/** TODO:. + * ADD WARNINGS for changed properties + * ADD VisualAlgo visualizations to RESOURCES!!! + * remove semi-colons + * change modal message to something more appropriate -> create and point to "change log" + * fix circular list edge cases: + - remove from single-node list with remove or removeAt + - no match for remove method, return null and don't decrement + * any other LL fixes??? + * add return null if element exists to all LL + * + * POST UPDATE RELEASE: + * toggle editor theme + * add Menu Searh / Filter + * switch to real JSDoc, provide Markdown docs + * refactor modal into separate components: announcement, resources + * rework application structure, add most state to top level + * find a way around below hack + */ + +// HACK: For preventing text highlighting on mousemove when +// dragging dividers: setting this as a key in component state +// & manipulating via the mousedown event handler caused the +// component to throw an error about setting state on an unmounted +// component, even though the state is being set in the top level +// app and it must be mounted because it's still rendered to the +// DOM. Not sure why this is happening right now, but this hack +// is a workaround that I can live with for the time being. +let disableHighlightText = false class App extends Component { + handleMousedownEvent = (e) => { + if (e.target.classList.contains('divider')) { + disableHighlightText = true + } + } + handleMouseupEvent = (e) => { + disableHighlightText = false + } + handleMousemoveEvent = (e) => { + if (disableHighlightText) { + e.preventDefault() + } + } componentDidMount() { - // pass refs to simple drag function - // to allow for AWESOME pane resizing - resizePanes( - this.leftPane, - this.topPane, - this.rightPane, - this.bottomPane, - this.verticalDivider, - this.horizontalDivider - ); - // count hits to live site using node server + // register event listeners: + document.addEventListener('mouseup', this.handleMouseupEvent) + document.addEventListener('mousemove', this.handleMousemoveEvent) + document.addEventListener('mousedown', this.handleMousedownEvent) + // double-click event for snapping divider top or bottom + this.horizontalDivider.addEventListener('dblclick', this.props.doubleClick) + // apply simpleDrag to allow for AWESOME pane resizing: + this.horizontalDivider.simpleDrag(dragVertical, null, 'vertical') + this.verticalDivider.simpleDrag(dragHorizontal, null, 'horizontal') + // render announcement modal 1st 3 visits after changes: + renderAnnouncementUtil() + // register hits to hit-count-server: if (process.env.NODE_ENV === 'production') { axios.post('https://hit-count-server.herokuapp.com/register-count') .then(() => null) - .catch(() => null); + .catch(() => null) } } + componentWillUnmount() { + // de-register event listeners: + document.removeEventListener('mouseup', this.handleMouseupEvent) + document.removeEventListener('mousedown', this.handleMousedownEvent) + document.removeEventListener('mousemove', this.handleMousemoveEvent) + this.horizontalDivider.removeEventListener('dblclick', this.props.doubleClick) + } render() { - return [ - , - this.verticalDivider = ref } - direction="vertical" - key={shortid.generate()} />, -
this.rightPane = ref }> - - -
, - - ]; + attachRef={ref => this.verticalDivider = ref} + direction="vertical" /> + + + + + + + ) } } // NOTE: Modal is Portal rendered within #modal-root, not the app #root // It WILL NOT be rendered alongside the other components in this tree -export default App; +export default connect(null, { doubleClick })(App) + +// export default connect( +// ({ modal: { renderModal } }) => ({ renderModal }), +// { doubleClick }) +// (App) diff --git a/src/App.test.js b/src/App.test.js deleted file mode 100644 index b84af98..0000000 --- a/src/App.test.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import App from './App'; - -it('renders without crashing', () => { - const div = document.createElement('div'); - ReactDOM.render(, div); -}); diff --git a/src/__tests__/AlgorithmChallenges.test.js b/src/__tests__/AlgorithmChallenges.test.js new file mode 100644 index 0000000..5af4983 --- /dev/null +++ b/src/__tests__/AlgorithmChallenges.test.js @@ -0,0 +1,19 @@ +import createJestTest from '../utils/test/app/create-jest-test' +import { CODE } from '../assets/codeRef' +import { forEach } from 'lodash'; + +const IDS = [ + 'AnagramPalindrome', + 'GenerateCheckerboard', + 'NoTwoConsecutiveChars', + 'SumAllPrimes', +] + +describe('Algorithm Challenges: Solution Code Passes Tests', () => + forEach(IDS, createJestTest)) + +test('All Algorithm Challenges are being tested', () => + expect(IDS.length).toEqual( + CODE.EASY_ALGOS.length + + CODE.MODERATE_ALGOS.length) + ) diff --git a/src/__tests__/DataStructures.test.js b/src/__tests__/DataStructures.test.js new file mode 100644 index 0000000..38c92d0 --- /dev/null +++ b/src/__tests__/DataStructures.test.js @@ -0,0 +1,21 @@ +import createJestTest from '../utils/test/app/create-jest-test' +import { CODE } from '../assets/codeRef'; +import { forEach } from 'lodash'; + +const IDS = [ + 'BinarySearchTree', + 'DoublyLinkedList', + 'Graph', + 'HashTable', + 'LinkedList', + 'MaxHeap', + 'PriorityQueue', + 'Queue', + 'Stack' +] + +describe('Data Structures: Solution Code Passes Tests', () => + forEach(IDS, createJestTest)) + +test('All Data Structures are being tested', () => + expect(IDS.length).toEqual(CODE.DATA_STRUCTURES.length)) diff --git a/src/__tests__/SortingAlgorithms.test.js b/src/__tests__/SortingAlgorithms.test.js new file mode 100644 index 0000000..51e98f5 --- /dev/null +++ b/src/__tests__/SortingAlgorithms.test.js @@ -0,0 +1,19 @@ +import createJestTest from '../utils/test/app/create-jest-test' +import { CODE } from '../assets/codeRef' +import { forEach } from 'lodash'; + +const IDS = [ + 'BubbleSort', + 'BucketSort', + 'HeapSort', + 'InsertionSort', + 'Mergesort', + 'Quicksort', + 'SelectionSort', +] + +describe('Sorting Algorithms: Solution Code Passes Tests', () => + forEach(IDS, createJestTest)) + +test('All Sorting Algos are being tested', () => + expect(IDS.length).toEqual(CODE.SORTING_ALGOS.length-1)) diff --git a/src/actions/console.js b/src/actions/console.js index 010378d..05c8974 100644 --- a/src/actions/console.js +++ b/src/actions/console.js @@ -1,26 +1,37 @@ -import { store } from '../index'; +import _ from 'lodash'; +import * as types from './types' +import { store } from '../index' +import { disableLogAction } from '../reducers/editor' -export const CLEAR_CONSOLE = 'CLEAR_CONSOLE'; -export const CONSOLE_LOG = 'CONSOLE_LOG'; +export const clearConsole = () => ({ type: types.CLEAR_CONSOLE }) -export const clearConsole = () => { - return { - type: CLEAR_CONSOLE - } -}; +_.mixin({ + 'createLogs': arr => + _.join( + _.map(arr, msg => { + // NOTE: Do not stringify DLL + if (typeof msg === 'object' + && msg !== null + && msg.hasOwnProperty('prev')) + return msg + else if (typeof msg !== 'string') + return JSON.stringify(msg) + return msg + }), + ' ' + ) +}) export const hijackConsole = () => { - const OG_Log = console.log; - console.log = function(...args) { - const messages = [...args].map(msg => { - return typeof msg !== 'string' - ? JSON.stringify(msg) - : msg - }).join(' '); - store.dispatch({ - type: CONSOLE_LOG, - messages - }); - OG_Log.apply(console, [...args]); - }; -}; + if (!disableLogAction) { + const OG_Log = console.log + console.log = function(...args) { + const logs = _.createLogs([...args]) + store.dispatch({ + type: types.CONSOLE_LOG, + logs + }) + OG_Log.apply(console, [...args]) + } + } +} diff --git a/src/actions/editor.js b/src/actions/editor.js index 86f8563..14bf758 100644 --- a/src/actions/editor.js +++ b/src/actions/editor.js @@ -1,45 +1,33 @@ -export const NEXT_SNIPPET = 'NEXT_SNIPPET'; -export const PREVIOUS_SNIPPET = 'PREVIOUS_SNIPPET'; -export const RESET_STATE = 'RESET_STATE'; -export const SELECT_SNIPPET = 'SELECT_SNIPPET'; -export const SELECT_SOLUTION = 'SELECT_SOLUTION'; -export const UPDATE_CODE = 'UPDATE_CODE'; +import * as types from './types' -export const selectSnippet = (id) => { - return { - type: SELECT_SNIPPET, - id - } -} +export const nextSnippet = () => ({ + type: types.NEXT_SNIPPET +}) -export const nextSnippet = (id) => { - return { - type: NEXT_SNIPPET - } -} +export const previousSnippet = () => ({ + type: types.PREVIOUS_SNIPPET +}) -export const previousSnippet = (id) => { - return { - type: PREVIOUS_SNIPPET - } -} +export const selectSnippet = (id) => ({ + type: types.SELECT_SNIPPET, + id +}) -export const updateCode = (code) => { - return { - type: UPDATE_CODE, - code - } -} +export const selectSolution = (id) => ({ + type: types.SELECT_SOLUTION, + id +}) -export const selectSolution = (id) => { - return { - type: SELECT_SOLUTION, - id: id.slice(10) - } -} +export const toggleSolution = (id) => ({ + type: types.TOGGLE_SOLUTION, + id +}) -export const resetEditorState = () => { - return { - type: RESET_STATE - } -} +export const resetEditorState = () => ({ + type: types.RESET_STATE +}) + +export const updateCode = (code) => ({ + type: types.UPDATE_CODE, + code +}) diff --git a/src/actions/modal.js b/src/actions/modal.js index e94bd25..e0bbc37 100644 --- a/src/actions/modal.js +++ b/src/actions/modal.js @@ -1,15 +1,48 @@ -export const OPEN_MODAL = 'OPEN_MODAL'; -export const CLOSE_MODAL = 'CLOSE_MODAL'; +import * as types from './types' +import { store } from '../index' +import { startCase } from 'lodash' -export const openModal = (id) => { - return { - type: OPEN_MODAL, - id - } -}; +export const openResourcesModal = (id) => ({ + type: types.OPEN_RESOURCES_MODAL, + id: startCase(id) +}) + +export const openAnnouncementModal = () => ({ + type: types.OPEN_ANNOUNCEMENT_MODAL, + id: 'Announcement!', + subHeader: messages[0], + messages: messages.slice(1) +}) + +export const closeModal = () => ({ type: types.CLOSE_MODAL }) -export const closeModal = () => { - return { - type: CLOSE_MODAL +// render announemnet util +// render only first 3 visits +export function renderAnnouncementUtil() { + const lsKey = 'cs-pg-react-render-only-thrice' + let ls = localStorage.getItem(lsKey) + if (!ls) { + localStorage.setItem(lsKey, 1) + store.dispatch(openAnnouncementModal()) + } else if (ls < 3) { + localStorage.setItem(lsKey, Number(ls) + 1) + store.dispatch(openAnnouncementModal()) } -}; +} + +const newIssue = 'https://github.com/no-stack-dub-sack/cs-playground-react/issues/new' + +const messages = [ + `CS-Playground-React has some new features! Some are rather + subtle improvements, but the most important of these is + Automated Testing:`, + `When this feature is enabled, every time you run your code, + a test suite will run in the background, and the results will + log to the console.`, + `NOTE: Tests are disabled by default to keep unwanted noise in the + console down to a minimum. Delete the // SUPPRESS TESTS +  comment to enable.`, + `I hope you enjoy! If you have comments or concerns, + feel free to open an issue.` +] diff --git a/src/actions/panes.js b/src/actions/panes.js new file mode 100644 index 0000000..12fe2df --- /dev/null +++ b/src/actions/panes.js @@ -0,0 +1,60 @@ +import * as types from './types' +import { store } from '../index' + +export const doubleClick = () => ({ type: types.DOUBLE_CLICK }) + +export const dragHorizontal = (el, pageX, startX, pageY, startY, resize) => { + resize.skipX = true + const LEFT_THRESHOLD = 30, RIGHT_THRESHOLD = 70 + if (pageX < window.innerWidth * LEFT_THRESHOLD / 100) { + pageX = window.innerWidth * LEFT_THRESHOLD / 100 + resize.pageX = pageX + } + if (pageX > window.innerWidth * RIGHT_THRESHOLD / 100) { + pageX = window.innerWidth * RIGHT_THRESHOLD / 100 + resize.pageX = pageX + } + + let cur = pageX / window.innerWidth * 100 + if (cur < 0) { + cur = 0 + } + if (cur > window.innerWidth) { + cur = window.innerWidth + } + + const right = 100 - cur - .5 + store.dispatch({ + type: types.DRAG_HORIZONTAL, + leftWidth: cur + "%", + rightWidth: right + "%" + }) +} + +export const dragVertical = (el, pageX, startX, pageY, startY, resize) => { + resize.skipY = true + const TOP_THRESHOLD = 0, BOTTOM_THRESHOLD = 100 + if (pageY < window.innerHeight * TOP_THRESHOLD / 100) { + pageY = window.innerHeight * TOP_THRESHOLD / 100 + resize.pageY = pageY + } + if (pageY > window.innerHeight * BOTTOM_THRESHOLD / 100) { + pageY = window.innerHeight * BOTTOM_THRESHOLD / 100 + resize.pageY = pageY + } + + let cur = pageY / window.innerHeight * 100 + if (cur < 0) { + cur = 0 + } + if (cur > window.innerHeight) { + cur = window.innerHeight + } + + const bottom = 100 - cur - 1 + store.dispatch({ + type: types.DRAG_VERTICAL, + topHeight: cur + "%", + bottomHeight: bottom + "%" + }) +} diff --git a/src/actions/resources.js b/src/actions/resources.js deleted file mode 100644 index dbb9ffa..0000000 --- a/src/actions/resources.js +++ /dev/null @@ -1,8 +0,0 @@ -export const SELECT_TOPIC = 'SELECT_TOPIC'; - -export const selectTopic = (id) => { - return { - type: SELECT_TOPIC, - id: id.replace(/_/g, ' ') - } -} diff --git a/src/actions/types.js b/src/actions/types.js new file mode 100644 index 0000000..f3ad972 --- /dev/null +++ b/src/actions/types.js @@ -0,0 +1,22 @@ +// console +export const CLEAR_CONSOLE = 'CLEAR_CONSOLE' +export const CONSOLE_LOG = 'CONSOLE_LOG' + +// editor +export const NEXT_SNIPPET = 'NEXT_SNIPPET' +export const PREVIOUS_SNIPPET = 'PREVIOUS_SNIPPET' +export const RESET_STATE = 'RESET_STATE' +export const SELECT_SNIPPET = 'SELECT_SNIPPET' +export const SELECT_SOLUTION = 'SELECT_SOLUTION' +export const UPDATE_CODE = 'UPDATE_CODE' +export const TOGGLE_SOLUTION = 'TOGGLE_SOLUTION' + +// modal +export const OPEN_RESOURCES_MODAL = 'OPEN_RESOURCES_MODAL' +export const OPEN_ANNOUNCEMENT_MODAL = 'OPEN_ANNOUNCEMENT_MODAL' +export const CLOSE_MODAL = 'CLOSE_MODAL' + +// panes +export const DRAG_VERTICAL = 'DRAG_VERTICAL' +export const DRAG_HORIZONTAL = 'DRAG_HORIZONTAL' +export const DOUBLE_CLICK = 'DOUBLE_CLICK' diff --git a/src/assets/codeRef.js b/src/assets/codeRef.js index 2bfc395..a63e080 100644 --- a/src/assets/codeRef.js +++ b/src/assets/codeRef.js @@ -1,30 +1,32 @@ -import BubbleSort from './seed/algorithms/BubbleSort'; -import BucketSort from './seed/algorithms/BucketSort'; -import HeapSort from './seed/algorithms/HeapSort'; -import InsertionSort from './seed/algorithms/InsertionSort'; -import Mergesort from './seed/algorithms/Mergesort'; -import Quicksort from './seed/algorithms/Quicksort'; -import SelectionSort from './seed/algorithms/SelectionSort'; -import SortingAlgorithmBenchmarks from './seed/algorithms/SortingBenchmarks'; +import { forEach, replace } from 'lodash' -import BinarySearchTree from './seed/data-structures/BinarySearchTree'; -import DoublyLinkedList from './seed/data-structures/DoublyLinkedList'; +import BubbleSort from './seed/algorithms/BubbleSort' +import BucketSort from './seed/algorithms/BucketSort' +import HeapSort from './seed/algorithms/HeapSort' +import InsertionSort from './seed/algorithms/InsertionSort' +import Mergesort from './seed/algorithms/Mergesort' +import Quicksort from './seed/algorithms/Quicksort' +import SelectionSort from './seed/algorithms/SelectionSort' +import SortingAlgorithmBenchmarks from './seed/algorithms/SortingBenchmarks' + +import BinarySearchTree from './seed/data-structures/BinarySearchTree' +import DoublyLinkedList from './seed/data-structures/DoublyLinkedList' import Graph from './seed/data-structures/Graph' -import HashTable from './seed/data-structures/HashTable'; -import LinkedList from './seed/data-structures/LinkedList'; -import MaxHeap from './seed/data-structures/MaxHeap'; -import PriorityQueue from './seed/data-structures/PriorityQueue'; -import Queue from './seed/data-structures/Queue'; -import Stack from './seed/data-structures/Stack'; +import HashTable from './seed/data-structures/HashTable' +import LinkedList from './seed/data-structures/LinkedList' +import MaxHeap from './seed/data-structures/MaxHeap' +import PriorityQueue from './seed/data-structures/PriorityQueue' +import Queue from './seed/data-structures/Queue' +import Stack from './seed/data-structures/Stack' -import AnagramPalindrome from './seed/algorithms/AnagramPalindrome'; -import NoTwoConsecutiveChars from './seed/algorithms/NoTwoConsecutiveChars'; -import SumAllPrimes from './seed/algorithms/SumAllPrimes'; -import GenerateCheckerboard from './seed/algorithms/GenerateCheckerboard'; +import AnagramPalindrome from './seed/algorithms/AnagramPalindrome' +import NoTwoConsecutiveChars from './seed/algorithms/NoTwoConsecutiveChars' +import SumAllPrimes from './seed/algorithms/SumAllPrimes' +import GenerateCheckerboard from './seed/algorithms/GenerateCheckerboard' // NOTE: order of arrays determines order of sidebar menu -export default { +export const CODE = { SORTING_ALGOS: [ Quicksort, Mergesort, @@ -55,7 +57,22 @@ export default { MODERATE_ALGOS: [ NoTwoConsecutiveChars, AnagramPalindrome, - // SumPrimeFactors, // RotateAnImage, ] -}; +} + +const createSolutionsRef = () => { + const results = {} + for (let category in CODE) { + forEach( + CODE[category], + topic => + results[ + replace(topic.title, /\s/g, '') + ] = topic.solution + ) + } + return results +} + +export const SOLUTIONS = createSolutionsRef() diff --git a/src/assets/seed/algorithms/AnagramPalindrome.js b/src/assets/seed/algorithms/AnagramPalindrome.js index 0a310c6..f34a367 100644 --- a/src/assets/seed/algorithms/AnagramPalindrome.js +++ b/src/assets/seed/algorithms/AnagramPalindrome.js @@ -15,17 +15,17 @@ export default { /** * @function anagramPalindrome * @param {string} str - * @return {boolean} + * @returns {boolean} */ function anagramPalindrome(str) { - return true; + return true } -console.log(anagramPalindrome('armdabbmaboobrd')); // bombard a drab mob => true -console.log(anagramPalindrome('armdabsbmaboobrd')); // bombards a drab mob => false -console.log(anagramPalindrome('tdolgsaetagdliadaoasaasinvdeavn')); // a santa dog lived as a devil god at nasa => true -console.log(anagramPalindrome('raoistddtagstonveakaaeawfewosln')); // a santa dog lived at nasa for two weeks => false +console.log(anagramPalindrome('armdabbmaboobrd')) // bombard a drab mob => true +console.log(anagramPalindrome('armdabsbmaboobrd')) // bombards a drab mob => false +console.log(anagramPalindrome('tdolgsaetagdliadaoasaasinvdeavn')) // a santa dog lived as a devil god at nasa => true +console.log(anagramPalindrome('raoistddtagstonveakaaeawfewosln')) // a santa dog lived at nasa for two weeks => false `, solution: `// given a ramdom string of letters, return true if the letters @@ -42,35 +42,35 @@ console.log(anagramPalindrome('raoistddtagstonveakaaeawfewosln')); // a santa do /** * @function anagramPalindrome * @param {string} str - * @return {boolean} + * @returns {boolean} */ function anagramPalindrome(str) { - var freq = {}, odds = 0; + var freq = {}, odds = 0 - for (var letter of str) { - freq[letter] = -~freq[letter]; + for (let letter of str) { + freq[letter] = -~freq[letter] } - for (var letter in freq) { + for (let letter in freq) { if (freq[letter] % 2 !== 0) { - odds++; + odds++ if (odds > 1) { - return false; + return false } } } - return true; + return true } -console.log(anagramPalindrome('armdabbmaboobrd')); // bombard a drab mob -console.log(anagramPalindrome('armdabsbmaboobrd')); // bombards a drab mob -console.log(anagramPalindrome('tdolgsaetagdliadaoasaasinvdeavn')); // a santa dog lived as a devil god at nasa -console.log(anagramPalindrome('raoistddtagstonveakaaeawfewosln')); // a santa dog lived at nasa for two weeks +console.log(anagramPalindrome('armdabbmaboobrd')) // bombard a drab mob +console.log(anagramPalindrome('armdabsbmaboobrd')) // bombards a drab mob +console.log(anagramPalindrome('tdolgsaetagdliadaoasaasinvdeavn')) // a santa dog lived as a devil god at nasa +console.log(anagramPalindrome('raoistddtagstonveakaaeawfewosln')) // a santa dog lived at nasa for two weeks `, resources: [ { href: 'https://www.hackerrank.com/challenges/game-of-thrones/problem', caption: 'Hackerrank Challenge' }, { href: 'http://www.geeksforgeeks.org/check-given-string-rotation-palindrome/', caption: 'GeeksForGeeks.org' }, ] -}; +} diff --git a/src/assets/seed/algorithms/BubbleSort.js b/src/assets/seed/algorithms/BubbleSort.js index 9ca82fd..d182b5d 100644 --- a/src/assets/seed/algorithms/BubbleSort.js +++ b/src/assets/seed/algorithms/BubbleSort.js @@ -4,39 +4,39 @@ export default { `/** * @function bubbleSort * @param {number[]} arr - * @return {number[]} + * @returns {number[]} */ function bubbleSort(arr) { - return arr; + return arr } -console.log(bubbleSort([23, 563, 0, 0, 2, 29, 8, 67, 22, 345, 11, 9, 53, 8])); +console.log(bubbleSort([23, 563, 0, 0, 2, 29, 8, 67, 22, 345, 11, 9, 53, 8])) `, solution: `/** * @function bubbleSort * @param {number[]} arr - * @return {number[]} + * @returns {number[]} */ function bubbleSort(arr) { - var swapped = true; + var swapped = true while (swapped) { - swapped = false; + swapped = false for (let i = 0; i < arr.length; i++) { if (arr[i] > arr[i+1]) { - [ arr[i], arr[i+1] ] = [ arr[i+1], arr[i] ]; - swapped = true; + [ arr[i], arr[i+1] ] = [ arr[i+1], arr[i] ] + swapped = true } } } - return arr; + return arr } -console.log(bubbleSort([23, 563, 0, 0, 2, 29, 8, 67, 22, 345, 11, 9, 53, 8])); +console.log(bubbleSort([23, 563, 0, 0, 2, 29, 8, 67, 22, 345, 11, 9, 53, 8])) `, resources: [ { href: 'http://www.geeksforgeeks.org/bubble-sort/', caption: 'GeeksforGeeks.org'}, @@ -45,5 +45,6 @@ console.log(bubbleSort([23, 563, 0, 0, 2, 29, 8, 67, 22, 345, 11, 9, 53, 8])); { href: 'https://en.wikipedia.org/wiki/Bubble_sort', caption: 'Wikipedia'}, { href: 'https://guide.freecodecamp.org/algorithms/sorting-algorithms/bubble-sort/', caption: 'freeCodeCamp Guides'}, { href: 'https://www.cs.usfca.edu/~galles/visualization/ComparisonSort.html', caption: 'Awesome Sorting Algo Visualizations!'}, + { href: 'https://visualgo.net/en/sorting', caption: 'VisualAlgo.net Sorting Algo Visualizations!'}, ] -}; +} diff --git a/src/assets/seed/algorithms/BucketSort.js b/src/assets/seed/algorithms/BucketSort.js index f449605..72320db 100644 --- a/src/assets/seed/algorithms/BucketSort.js +++ b/src/assets/seed/algorithms/BucketSort.js @@ -4,7 +4,7 @@ export default { `/** * @function bucketSort * @param {number[]} arr - * @return {number[]} + * @returns {number[]} */ // there are many different implementations of this algorithm, this is just one! @@ -15,7 +15,7 @@ export default { // here would fail with a set of whole numbers which are not evenly distrubed. function bucketSort(arr) { - return arr; + return arr } console.log( @@ -24,13 +24,13 @@ console.log( 0.23, 0.88, 0.47, 0.52, 0.72, 0.99, 0.63, 0.45, 0.21, 0.12, 0.23, 0.94 ]) -); +) `, solution: `/** * @function bucketSort * @param {number[]} arr - * @return {number[]} + * @returns {number[]} */ // there are many different implementations of this algorithm, this is just one! @@ -41,27 +41,27 @@ console.log( // here would fail with a set of whole numbers which are not evenly distrubed. function bucketSort(arr) { - const len = arr.length; - let buckets = [...Array(len)].map(i => Array()); + const len = arr.length + let buckets = [...Array(len)].map(i => Array()) for (let i = 0; i < arr.length; i++) { - let bucket = Math.floor(len*arr[i]); - buckets[bucket].push(arr[i]); + let bucket = Math.floor(len*arr[i]) + buckets[bucket].push(arr[i]) } for (let bucket of buckets) { if (bucket.length) { - insertionSort(bucket); + insertionSort(bucket) } } - return buckets.reduce((result, el) => result.concat(el), []); + return buckets.reduce((result, el) => result.concat(el), []) } /** * @function insertionSort * @param {number[]} arr - * @return {number[]} + * @returns {number[]} */ // we use insertion sort to sort each bucket. @@ -70,14 +70,14 @@ function bucketSort(arr) { function insertionSort(arr) { for (let i = 0; i < arr.length; i++) { - let j = i+1; + let j = i+1 while (arr[j] < arr[j-1]) { - [ arr[j], arr[j-1] ] = [ arr[j-1], arr[j] ]; - j--; + [ arr[j], arr[j-1] ] = [ arr[j-1], arr[j] ] + j-- } } - return arr; + return arr } console.log( @@ -86,7 +86,7 @@ console.log( 0.23, 0.88, 0.47, 0.52, 0.72, 0.99, 0.63, 0.45, 0.21, 0.12, 0.23, 0.94 ]) -); +) `, resources: [ { href: 'http://www.geeksforgeeks.org/bucket-sort-2/', caption: 'GeeksforGeeks.org'}, @@ -94,4 +94,4 @@ console.log( { href: 'https://initjs.org/bucket-sort-in-javascript-dc040b8f0058', caption: 'Another Bucket Sort Implementation'}, { href: 'https://www.cs.usfca.edu/~galles/visualization/BucketSort.html', caption: 'Awesome Bucket Sort Visualizations!'}, ] -}; +} diff --git a/src/assets/seed/algorithms/GenerateCheckerboard.js b/src/assets/seed/algorithms/GenerateCheckerboard.js index 3f4c3ac..58b412b 100644 --- a/src/assets/seed/algorithms/GenerateCheckerboard.js +++ b/src/assets/seed/algorithms/GenerateCheckerboard.js @@ -4,7 +4,7 @@ export default { `/** @function generateCheckerboard * @param {number} h The height of the board * @param {number} w The width of the board - * @return {string} + * @returns {string} * * create an algorithm that takes two ints as * arguments, height and width, and returns a @@ -25,44 +25,44 @@ export default { */ function generateCheckerboard(h, w) { - return; + return } // change the h & w for different size boards! -const h = 8, w = 8; +const h = 8, w = 8 -console.log(generateCheckerboard(h, w)); +console.log(generateCheckerboard(h, w)) `, solution: `/** @function generateCheckerBoard * @param {number} h The height of the board * @param {number} w The width of the board - * @return {string} + * @returns {string} */ function generateCheckerboard(h, w) { - var row = '', board = ''; + var row = '', board = '' for (let i = 0; i < w; i++) { - row += "# "; + row += "# " } - row += '\\n'; + row += '\\n' for (let i = 0; i < h; i++) { if (i % 2 === 0) { - board += row; + board += row } else { - board += ' ' + row; + board += ' ' + row } } - return board; + return board } // change the h & w for different size boards! -const h = 8, w = 8; +const h = 8, w = 8 -console.log(generateCheckerboard(h, w)); +console.log(generateCheckerboard(h, w)) `, resources: [ { href: 'https://en.wikipedia.org/wiki/Checkerboard#/media/File:Checkerboard_pattern.svg', caption: 'Here\'s what a checkerboard looks like!'}, diff --git a/src/assets/seed/algorithms/HeapSort.js b/src/assets/seed/algorithms/HeapSort.js index c7d7584..ccdbf6c 100644 --- a/src/assets/seed/algorithms/HeapSort.js +++ b/src/assets/seed/algorithms/HeapSort.js @@ -3,135 +3,130 @@ export default { seed: `class MinHeap { constructor() { - this.heap = []; - this.length = 0; + this.heap = [] } // methods to implement: - // insert() <= where the magic happens! + // insert(number) <= where the magic happens! // remove() <= where the magic happens! // print() // sort() // size() } -var heap = new MinHeap(); +var heap = new MinHeap() -const unsorted = [72,3,19,24,99,45,33,0,2,43,17,19,22,80,100]; -// unsorted.forEach(num => heap.insert(num)); -// heap.print(); -// const sorted = heap.sort(); -// console.log('\\nheap sort: ' + JSON.stringify(sorted)); +const unsorted = [72,3,19,24,99,45,33,0,2,43,17,19,22,80,100] +// unsorted.forEach(num => heap.insert(num)) +// heap.print() +// const sorted = heap.sort() +// console.log('\\nheap sort: ' + JSON.stringify(sorted)) `, solution: `/** * @class MinHeap * @property {number[]} heap A collection of integers - * @property {number} length The length of the collection * @method insert @param {numnber} node - * @method remove @return {?number} returns null or the removed item + * @method remove @returns {?number} returns null or the removed item * @method print Logs the heap to the console - * @method sort @return {number[]} returns the sorted heap - * @method size @return {number} returns the size of the heap + * @method sort @returns {number[]} returns the sorted heap + * @method size @returns {number} returns the size of the heap */ class MinHeap { constructor() { - this.heap = []; - this.length = 0; + this.heap = [] } insert(node) { - this.heap.push(node); - this.length++; + this.heap.push(node) var swap = (node, nodeIdx) => { - var parentIdx = Math.floor((nodeIdx - 1) / 2); - var parent = this.heap[parentIdx]; + var parentIdx = Math.floor((nodeIdx - 1) / 2) + var parent = this.heap[parentIdx] if (parent > node) { - this.heap[parentIdx] = node; - this.heap[nodeIdx] = parent; - swap(node, parentIdx); + this.heap[parentIdx] = node + this.heap[nodeIdx] = parent + swap(node, parentIdx) } - }; + } - if (this.length > 1) { - return swap(node, this.length-1); + if (this.heap.length > 1) { + return swap(node, this.heap.length-1) } } remove() { if (!this.size) { - return null; + return null } - var min = this.heap.shift(); + var min = this.heap.shift() if (this.size > 1) { - this.heap.unshift(this.heap.pop()); + this.heap.unshift(this.heap.pop()) } var swap = (node, nodeIdx = 0) => { - var childIdx; + var childIdx if (this.size === 2) { - childIdx = 1; + childIdx = 1 } else if (this.heap[2 * nodeIdx + 1] < this.heap[2 * nodeIdx + 2]) { - childIdx = 2 * nodeIdx + 1; + childIdx = 2 * nodeIdx + 1 } else { - childIdx = 2 * nodeIdx + 2; + childIdx = 2 * nodeIdx + 2 } if (node > this.heap[childIdx]) { - this.heap[nodeIdx] = this.heap[childIdx]; - this.heap[childIdx] = node; - return swap(node, childIdx); + this.heap[nodeIdx] = this.heap[childIdx] + this.heap[childIdx] = node + return swap(node, childIdx) } - this.length--; - return min; + return min - }; + } - return swap(this.heap[0]); + return swap(this.heap[0]) } print() { - console.log('min heap: ' + JSON.stringify(this.heap)); - console.log('size: ' + this.size); + console.log('min heap: ' + JSON.stringify(this.heap)) + console.log('size: ' + this.size) } sort() { - var sorted = []; + var sorted = [] while (this.size) { - sorted.push(this.remove()); + sorted.push(this.remove()) } - return sorted; + return sorted } get size() { - return this.length; + return this.heap.length } } -var heap = new MinHeap(); +var heap = new MinHeap() -const unsorted = [72,3,19,24,99,45,33,0,2,43,17,19,22,80,100]; -unsorted.forEach(num => heap.insert(num)); +const unsorted = [72,3,19,24,99,45,33,0,2,43,17,19,22,80,100] +unsorted.forEach(num => heap.insert(num)) -heap.print(); +heap.print() -const sorted = heap.sort(); -console.log('\\nheap sort: ' + JSON.stringify(sorted)); +const sorted = heap.sort() +console.log('\\nheap sort: ' + JSON.stringify(sorted)) `, resources: [ { href: 'http://www.geeksforgeeks.org/heap-sort/', caption: 'GeeksforGeeks.org'}, @@ -141,4 +136,4 @@ console.log('\\nheap sort: ' + JSON.stringify(sorted)); { href: 'https://www.cs.usfca.edu/~galles/visualization/HeapSort.html', caption: 'Awesome Animated Sorting Algo Visualizations!'}, { href: 'https://www.hackerearth.com/practice/algorithms/sorting/heap-sort/tutorial/', caption: 'Another cool visualization'}, ] -}; +} diff --git a/src/assets/seed/algorithms/InsertionSort.js b/src/assets/seed/algorithms/InsertionSort.js index 0da9159..4535ff0 100644 --- a/src/assets/seed/algorithms/InsertionSort.js +++ b/src/assets/seed/algorithms/InsertionSort.js @@ -4,35 +4,35 @@ export default { `/** * @function insertionSort * @param {number[]} arr - * @return {number[]} + * @returns {number[]} */ function insertionSort(arr) { - return arr; + return arr } -console.log(insertionSort([56, 1, 2, 56, 767, 9, 9732, 0, 99, 11, 34, 87, 234, 1, 54])); +console.log(insertionSort([56, 1, 2, 56, 767, 9, 9732, 0, 99, 11, 34, 87, 234, 1, 54])) `, solution: `/** * @function insertionSort * @param {number[]} arr - * @return {number[]} + * @returns {number[]} */ function insertionSort(arr) { for (var i = 0; i < arr.length; i++) { - var j = i+1; + var j = i+1 while (arr[j] < arr[j-1]) { - [ arr[j], arr[j-1] ] = [ arr[j-1], arr[j] ]; - j--; + [ arr[j], arr[j-1] ] = [ arr[j-1], arr[j] ] + j-- } } - return arr; + return arr } -console.log(insertionSort([56, 1, 2, 56, 767, 9, 9732, 0, 99, 11, 34, 87, 234, 1, 54])); +console.log(insertionSort([56, 1, 2, 56, 767, 9, 9732, 0, 99, 11, 34, 87, 234, 1, 54])) `, resources: [ { href: 'http://www.geeksforgeeks.org/insertion-sort/', caption: 'GeeksforGeeks.org'}, @@ -41,5 +41,6 @@ console.log(insertionSort([56, 1, 2, 56, 767, 9, 9732, 0, 99, 11, 34, 87, 234, 1 { href: 'https://en.wikipedia.org/wiki/Insertion_sort', caption: 'Wikipedia'}, { href: 'https://guide.freecodecamp.org/algorithms/sorting-algorithms/insertion-sort/', caption: 'freeCodeCamp Guides'}, { href: 'https://www.cs.usfca.edu/~galles/visualization/ComparisonSort.html', caption: 'Awesome Sorting Algo Visualizations!'}, + { href: 'https://visualgo.net/en/sorting', caption: 'VisualAlgo.net Sorting Algo Visualizations!'}, ] -}; +} diff --git a/src/assets/seed/algorithms/Mergesort.js b/src/assets/seed/algorithms/Mergesort.js index b3cd8a9..4fcc7e0 100644 --- a/src/assets/seed/algorithms/Mergesort.js +++ b/src/assets/seed/algorithms/Mergesort.js @@ -4,57 +4,57 @@ export default { `/** * @function mergeSort * @param {number[]} arr - * @return {number[]} + * @returns {number[]} */ function mergeSort(arr) { - return arr; + return arr } -console.log(mergeSort([27698, 234, 98, 0, 23, 11, 9, 65, 3, 4, 0, 2, 1])); +console.log(mergeSort([27698, 234, 98, 0, 23, 11, 9, 65, 3, 4, 0, 2, 1])) `, solution: `/** * @function mergeSort * @param {number[]} arr - * @return {number[]} + * @returns {number[]} */ function mergeSort(arr) { if (arr.length < 2) { - return arr; + return arr } - var left = arr.slice(0, arr.length/2); - var right = arr.slice(arr.length/2); + var left = arr.slice(0, arr.length/2) + var right = arr.slice(arr.length/2) - return merge(mergeSort(left), mergeSort(right)); + return merge(mergeSort(left), mergeSort(right)) } /** * @function merge * @param {number[]} left * @param {number[]} right - * @return {number[]} + * @returns {number[]} */ function merge(left, right) { var results = [], idxL = 0, - idxR = 0; + idxR = 0 while (idxL < left.length && idxR < right.length) { if (left[idxL] <= right[idxR]) { - results.push(left[idxL++]); + results.push(left[idxL++]) } else { results.push(right[idxR++]) } } - return results.concat(left.slice(idxL), right.slice(idxR)); + return results.concat(left.slice(idxL), right.slice(idxR)) } -console.log(mergeSort([27698, 234, 98, 0, 23, 11, 9, 65, 3, 4, 0, 2, 1])); +console.log(mergeSort([27698, 234, 98, 0, 23, 11, 9, 65, 3, 4, 0, 2, 1])) `, resources: [ { href: 'http://www.geeksforgeeks.org/merge-sort/', caption: 'GeeksforGeeks.org'}, @@ -63,5 +63,6 @@ console.log(mergeSort([27698, 234, 98, 0, 23, 11, 9, 65, 3, 4, 0, 2, 1])); { href: 'https://en.wikipedia.org/wiki/Merge_sort', caption: 'Wikipedia'}, { href: 'https://guide.freecodecamp.org/algorithms/sorting-algorithms/merge-sort/', caption: 'freeCodeCamp Guides'}, { href: 'https://www.cs.usfca.edu/~galles/visualization/ComparisonSort.html', caption: 'Awesome Sorting Algo Visualizations!'}, + { href: 'https://visualgo.net/en/sorting', caption: 'VisualAlgo.net Sorting Algo Visualizations!'}, ] -}; +} diff --git a/src/assets/seed/algorithms/NoTwoConsecutiveChars.js b/src/assets/seed/algorithms/NoTwoConsecutiveChars.js index 30862a4..24cb153 100644 --- a/src/assets/seed/algorithms/NoTwoConsecutiveChars.js +++ b/src/assets/seed/algorithms/NoTwoConsecutiveChars.js @@ -16,20 +16,20 @@ export default { /** * @function noTwoConsecutiveChars * @param {string} str The String to operate on - * @return {(string|bool)} Rearranged string or false + * @returns {(string|bool)} Rearranged string or false */ function noTwoConsecutiveChars(str) { - return str; + return str } -console.log(noTwoConsecutiveChars('aaba')); -console.log(noTwoConsecutiveChars('aabba')); -console.log(noTwoConsecutiveChars('aaaaaaabbbbcc')); -console.log(noTwoConsecutiveChars('aaabaaabbbbbbbbbccccbbcbsd')); -console.log(noTwoConsecutiveChars('aaabaaabbbbbbbbbbccccbbcbsd')); -console.log(noTwoConsecutiveChars('aaabaaabbbbbbbbbbbccccbbcbsd')); +console.log(noTwoConsecutiveChars('aaba')) +console.log(noTwoConsecutiveChars('aabba')) +console.log(noTwoConsecutiveChars('aaaaaaabbbbcc')) +console.log(noTwoConsecutiveChars('aaabaaabbbbbbbbbccccbbcbsd')) +console.log(noTwoConsecutiveChars('aaabaaabbbbbbbbbbccccbbcbsd')) +console.log(noTwoConsecutiveChars('aaabaaabbbbbbbbbbbccccbbcbsd')) `, solution: `// Given a random string, return a new string containing all the @@ -47,61 +47,61 @@ console.log(noTwoConsecutiveChars('aaabaaabbbbbbbbbbbccccbbcbsd')); /** * @function noTwoConsecutiveChars * @param {string} str The String to operate on - * @return {(string|bool)} Rearranged string or false + * @returns {(string|bool)} Rearranged string or false */ function noTwoConsecutiveChars(str) { - var freqTable = {}; + var freqTable = {} for (var char of str) { - freqTable[char] = -~freqTable[char]; + freqTable[char] = -~freqTable[char] } function getNextChar(newStr = '', lastChar = '') { var mostFreq = 0, nextMostFreq = 0, - nextChar = ''; + nextChar = '' for (var char in freqTable) { if (freqTable[char] > mostFreq) { - mostFreq = freqTable[char]; - nextChar = char; + mostFreq = freqTable[char] + nextChar = char } if (nextChar === lastChar) { if (freqTable[char] > nextMostFreq && char !== lastChar) { - nextMostFreq = freqTable[char]; - nextChar = char; + nextMostFreq = freqTable[char] + nextChar = char } } } if (!mostFreq) { - return newStr; + return newStr } else if (nextChar === lastChar) { - return false; + return false } else { - newStr+=nextChar; - lastChar = nextChar; - freqTable[lastChar]--; - return getNextChar(newStr, lastChar); + newStr+=nextChar + lastChar = nextChar + freqTable[lastChar]-- + return getNextChar(newStr, lastChar) } } - return getNextChar(); + return getNextChar() } -console.log(noTwoConsecutiveChars('aaba')); -console.log(noTwoConsecutiveChars('aabba')); -console.log(noTwoConsecutiveChars('aaaaaaabbbbcc')); -console.log(noTwoConsecutiveChars('aaabaaabbbbbbbbbccccbbcbsd')); -console.log(noTwoConsecutiveChars('aaabaaabbbbbbbbbbccccbbcbsd')); -console.log(noTwoConsecutiveChars('aaabaaabbbbbbbbbbbccccbbcbsd')); +console.log(noTwoConsecutiveChars('aaba')) +console.log(noTwoConsecutiveChars('aabba')) +console.log(noTwoConsecutiveChars('aaaaaaabbbbcc')) +console.log(noTwoConsecutiveChars('aaabaaabbbbbbbbbccccbbcbsd')) +console.log(noTwoConsecutiveChars('aaabaaabbbbbbbbbbccccbbcbsd')) +console.log(noTwoConsecutiveChars('aaabaaabbbbbbbbbbbccccbbcbsd')) `, resources: [ { href: 'http://www.geeksforgeeks.org/rearrange-characters-string-no-two-adjacent/', caption: 'GeeksforGeeks.org'}, ] -}; +} diff --git a/src/assets/seed/algorithms/Quicksort.js b/src/assets/seed/algorithms/Quicksort.js index 9c5a346..a493878 100644 --- a/src/assets/seed/algorithms/Quicksort.js +++ b/src/assets/seed/algorithms/Quicksort.js @@ -4,14 +4,14 @@ export default { `/** * @function quickSort * @param {number[]} arr - * @return {number[]} + * @returns {number[]} */ function quickSort(arr) { - return arr; + return arr } -console.log(quickSort([6, 9, 23, 3564, 0, 4, 99, 11, 25, 74, 939, 35, 1, 643, 3, 75])); +console.log(quickSort([6, 9, 23, 3564, 0, 4, 99, 11, 25, 74, 939, 35, 1, 643, 3, 75])) `, solution: `/** @@ -19,25 +19,25 @@ console.log(quickSort([6, 9, 23, 3564, 0, 4, 99, 11, 25, 74, 939, 35, 1, 643, 3, * @param {number[]} arr * @param {number} [low=0] * @param {number} [high=arr.length] - * @return {number[]} + * @returns {number[]} */ function quickSort(arr, low = 0, high = arr.length-1) { if (arr.length > 1) { - var index = partition(arr, low, high); + var index = partition(arr, low, high) if (low < index - 1) { - quickSort(arr, low, index-1); + quickSort(arr, low, index-1) } if (high > index) { - quickSort(arr, index, high); + quickSort(arr, index, high) } } - return arr; + return arr } /** @@ -45,36 +45,36 @@ function quickSort(arr, low = 0, high = arr.length-1) { * @param {number[]} arr * @param {number} [low=0] * @param {number} [high=arr.length] - * @return {number[]} + * @returns {number[]} */ function partition(arr, low, high) { var pivot = arr[Math.floor((low+high)/2)], i = low, - j = high; + j = high while (i <= j) { while (arr[i] < pivot) { - i++; + i++ } while (arr[j] > pivot) { - j--; + j-- } if (i <= j) { - [ arr[i], arr[j] ] = [ arr[j], arr[i] ]; - j--; - i++; + [ arr[i], arr[j] ] = [ arr[j], arr[i] ] + j-- + i++ } } - return i; + return i } -console.log(quickSort([6, 9, 23, 3564, 0, 4, 99, 11, 25, 74, 939, 35, 1, 643, 3, 75])); +console.log(quickSort([6, 9, 23, 3564, 0, 4, 99, 11, 25, 74, 939, 35, 1, 643, 3, 75])) `, resources: [ { href: 'http://www.geeksforgeeks.org/quick-sort/', caption: 'GeeksforGeeks.org'}, @@ -83,6 +83,7 @@ console.log(quickSort([6, 9, 23, 3564, 0, 4, 99, 11, 25, 74, 939, 35, 1, 643, 3, { href: 'https://en.wikipedia.org/wiki/Quicksort', caption: 'Wikipedia'}, { href: 'https://guide.freecodecamp.org/algorithms/sorting-algorithms/quick-sort', caption: 'freeCodeCamp Guides'}, { href: 'https://www.cs.usfca.edu/~galles/visualization/ComparisonSort.html', caption: 'Awesome Sorting Algo Visualizations!'}, + { href: 'https://visualgo.net/en/sorting', caption: 'VisualAlgo.net Sorting Algo Visualizations!'}, { href: 'https://www.youtube.com/watch?v=MZaf_9IZCrc', caption: 'Youtube Quicksort Visualization'}, ] -}; +} diff --git a/src/assets/seed/algorithms/SelectionSort.js b/src/assets/seed/algorithms/SelectionSort.js index 933894b..a30eb08 100644 --- a/src/assets/seed/algorithms/SelectionSort.js +++ b/src/assets/seed/algorithms/SelectionSort.js @@ -4,46 +4,46 @@ export default { `/** * @function selectionSort * @param {number[]} arr - * @return {number[]} + * @returns {number[]} */ function selectionSort(arr) { - return arr; + return arr } -console.log(selectionSort([5, 23, 9876, 21, 0, 11, 2, 67, 89, 234, 0, 12, 43, 694])); +console.log(selectionSort([5, 23, 9876, 21, 0, 11, 2, 67, 89, 234, 0, 12, 43, 694])) `, solution: `/** * @function selectionSort * @param {number[]} arr - * @return {number[]} + * @returns {number[]} */ function selectionSort(arr) { - var pointer = 0; + var pointer = 0 while (pointer < arr.length) { - var min = arr[pointer], swapIdx; + var min = arr[pointer], swapIdx for (var i = pointer; i < arr.length; i++) { if (arr[i] <= min) { - min = arr[i]; - swapIdx = i; + min = arr[i] + swapIdx = i } } - var temp = arr[pointer]; - arr[pointer] = min; - arr[swapIdx] = temp; - pointer++; + var temp = arr[pointer] + arr[pointer] = min + arr[swapIdx] = temp + pointer++ } - return arr; + return arr } // NOTE: If your solution looks a little different than this, you can see a couple of other correct but -// slightly modified implementations of selection sort here: https://repl.it/@no_stack_dub_sack/Selection-Sort. +// slightly modified implementations of selection sort here: https://repl.it/@no_stack_dub_sack/Selection-Sort. -console.log(selectionSort([5, 23, 9876, 21, 0, 11, 2, 67, 89, 234, 0, 12, 43, 694])); +console.log(selectionSort([5, 23, 9876, 21, 0, 11, 2, 67, 89, 234, 0, 12, 43, 694])) `, resources: [ { href: 'http://www.geeksforgeeks.org/selection-sort/', caption: 'GeeksforGeeks.org'}, @@ -52,5 +52,6 @@ console.log(selectionSort([5, 23, 9876, 21, 0, 11, 2, 67, 89, 234, 0, 12, 43, 69 { href: 'https://en.wikipedia.org/wiki/Selection_sort', caption: 'Wikipedia'}, { href: 'https://guide.freecodecamp.org/algorithms/sorting-algorithms/selection-sort/', caption: 'freeCodeCamp Guides'}, { href: 'https://www.cs.usfca.edu/~galles/visualization/ComparisonSort.html', caption: 'Awesome Sorting Algo Visualizations!'}, + { href: 'https://visualgo.net/en/sorting', caption: 'VisualAlgo.net Sorting Algo Visualizations!'}, ] -}; +} diff --git a/src/assets/seed/algorithms/SortingBenchmarks.js b/src/assets/seed/algorithms/SortingBenchmarks.js index 95ffa5d..c1b2efb 100644 --- a/src/assets/seed/algorithms/SortingBenchmarks.js +++ b/src/assets/seed/algorithms/SortingBenchmarks.js @@ -4,29 +4,29 @@ export default { `// MERGESORT / MERGE function mergeSort(arr) { if (arr.length < 2) { - return arr; + return arr } - var left = arr.slice(0, arr.length/2); - var right = arr.slice(arr.length/2); + var left = arr.slice(0, arr.length/2) + var right = arr.slice(arr.length/2) - return merge(mergeSort(left), mergeSort(right)); + return merge(mergeSort(left), mergeSort(right)) } function merge(left, right) { var results = [], il = 0, - ir = 0; + ir = 0 while (il < left.length && ir < right.length) { if (left[il] <= right[ir]) { - results.push(left[il++]); + results.push(left[il++]) } else { - results.push(right[ir++]); + results.push(right[ir++]) } } - return results.concat(left.slice(il), right.slice(ir)); + return results.concat(left.slice(il), right.slice(ir)) } @@ -36,84 +36,84 @@ function quickSort(arr, low = 0, high = arr.length-1) { if (arr.length > 1) { - var index = partition(arr, low, high); + var index = partition(arr, low, high) if (low < index - 1) { - quickSort(arr, low, index-1); + quickSort(arr, low, index-1) } if (high > index) { - quickSort(arr, index, high); + quickSort(arr, index, high) } } - return arr; + return arr } function partition(arr, low, high) { var pivot = arr[Math.floor((low+high)/2)], i = low, - j = high; + j = high while (i <= j) { while (arr[i] < pivot) { - i++; + i++ } while (arr[j] > pivot) { - j--; + j-- } if (i <= j) { - swap(arr, i, j); - j--; - i++; + swap(arr, i, j) + j-- + i++ } } - return i; + return i } // BUBBLE SORT function bubbleSort(arr) { - var swapped = true; + var swapped = true while (swapped) { - swapped = false; + swapped = false for (let i = 0; i < arr.length; i++) { if (arr[i] > arr[i+1]) { - swap(arr, i, i+1); - swapped = true; + swap(arr, i, i+1) + swapped = true } } } - return arr; + return arr } // SELECTION SORT function selectionSort(arr) { - var pointer = 0; + var pointer = 0 while (pointer < arr.length) { - var min = arr[pointer], swapIdx; + var min = arr[pointer], swapIdx for (var i = pointer; i < arr.length; i++) { if (arr[i] <= min) { - min = arr[i]; - swapIdx = i; + min = arr[i] + swapIdx = i } } - swap(arr, pointer, swapIdx); - pointer++; + swap(arr, pointer, swapIdx) + pointer++ } - return arr; + return arr } @@ -121,14 +121,14 @@ function selectionSort(arr) { // INSERTION SORT function insertionSort(arr) { for (var i = 0; i < arr.length; i++) { - var j = i+1; + var j = i+1 while (arr[j] < arr[j-1]) { - swap(arr, j, j-1); - j--; + swap(arr, j, j-1) + j-- } } - return arr; + return arr } @@ -136,86 +136,86 @@ function insertionSort(arr) { // HEAP SORT class MinHeap { constructor() { - this.heap = []; - this.length = 0; + this.heap = [] + this.length = 0 } insert(node) { - this.heap.push(node); - this.length++; + this.heap.push(node) + this.length++ var swap = (node, nodeIdx) => { - var parentIdx = Math.floor((nodeIdx - 1) / 2); - var parent = this.heap[parentIdx]; + var parentIdx = Math.floor((nodeIdx - 1) / 2) + var parent = this.heap[parentIdx] if (parent > node) { - this.heap[parentIdx] = node; - this.heap[nodeIdx] = parent; - swap(node, parentIdx); + this.heap[parentIdx] = node + this.heap[nodeIdx] = parent + swap(node, parentIdx) } - }; + } if (this.length > 1) { - return swap(node, this.length-1); + return swap(node, this.length-1) } } remove() { if (!this.size) { - return null; + return null } - var min = this.heap.shift(); + var min = this.heap.shift() if (this.size > 1) { - this.heap.unshift(this.heap.pop()); + this.heap.unshift(this.heap.pop()) } var swap = (node, nodeIdx = 0) => { - var childIdx; + var childIdx if (this.size === 2) { - childIdx = 1; + childIdx = 1 } else if (this.heap[2 * nodeIdx + 1] < this.heap[2 * nodeIdx + 2]) { - childIdx = 2 * nodeIdx + 1; + childIdx = 2 * nodeIdx + 1 } else { - childIdx = 2 * nodeIdx + 2; + childIdx = 2 * nodeIdx + 2 } if (node > this.heap[childIdx]) { - this.heap[nodeIdx] = this.heap[childIdx]; - this.heap[childIdx] = node; - return swap(node, childIdx); + this.heap[nodeIdx] = this.heap[childIdx] + this.heap[childIdx] = node + return swap(node, childIdx) } - this.length--; - return min; + this.length-- + return min - }; + } - return swap(this.heap[0]); + return swap(this.heap[0]) } sort(arr) { - var sorted = []; + var sorted = [] for (var i = 0; i < arr.length; i++) { - this.insert(arr[i]); + this.insert(arr[i]) } while (this.size) { - sorted.push(this.remove()); + sorted.push(this.remove()) } - return sorted; + return sorted } get size() { - return this.length; + return this.length } } @@ -226,27 +226,27 @@ class MinHeap { // swaps array items function swap(arr, a, b) { - var temp = arr[a]; - arr[a] = arr[b]; - arr[b] = temp; + var temp = arr[a] + arr[a] = arr[b] + arr[b] = temp } // generate random 5000 item array for benchmarking against const BENCH_ARRAY = (function randomizeArray(n) { - var arr = []; + var arr = [] while (arr.length < n) { - arr.push(Math.floor(Math.random() * n + 1)); + arr.push(Math.floor(Math.random() * n + 1)) } - return arr; -})(5000); + return arr +})(5000) // parseTime utility function parseTime(start, end) { - return parseInt(end - start).toString() + 'ms'; + return parseInt(end - start).toString() + 'ms' } // BENCHMARKS: @@ -259,105 +259,30 @@ const algos = [ { title: 'Heap Sort', heap: new MinHeap() }, // ~13ms // Array.sort is implemented w/ a highly optimized merge sort in most engines { title: 'Array.sort', array: [...BENCH_ARRAY] }, // ~2ms WOW!!! -]; +] algos.forEach(el => { // we make sure to make a copy of the array // on each iteration, otherwise it will // already be sorted by the time we // reach the second algorithm! - let sortMe = [...BENCH_ARRAY]; + let sortMe = [...BENCH_ARRAY] - /******/ let START = window.performance.now(); /******/ + /******/ let START = window.performance.now() /******/ if (el.array) { - el.array.sort((a, b) => a > b); + el.array.sort((a, b) => a > b) } else if (el.heap) { el.heap.sort(sortMe) } else { - el.func(sortMe); + el.func(sortMe) } - /******/ let END = window.performance.now(); /******/ + /******/ let END = window.performance.now() /******/ - console.log(el.title + ': ' + parseTime(START, END)); -}); + console.log(el.title + ': ' + parseTime(START, END)) +}) `, solution: '', resources: [] -}; - - - -// BENCHMARKS: -// const algos = [ -// { title: 'Mergesort', func: mergeSort }, // ~10ms -// { title: 'Quicksort', func: quickSort }, // ~5ms -// { title: 'Bubble Sort', func: bubbleSort }, // ~325ms -// { title: 'Selection Sort', func: selectionSort }, // ~20ms -// { title: 'Insertion Sort', func: insertionSort }, // ~120ms -// { title: 'Heap Sort', heap: new MinHeap() }, // ~13ms -// // Array.sort is implemented w/ a highly optimized merge sort in most engines -// { title: 'Array.sort', array: [...BENCH_ARRAY] }, // ~2ms WOW!!! -// ]; -// -// algos.forEach(el => { -// // be sure to make a copy of the benchmark -// // array on each iteration, otherwise it -// // will be sorted by the time we reach -// // the second algorithm! -// var sortMe = [...BENCH_ARRAY]; -// -// /******/ var START = window.performance.now(); /******/ -// -// if (el.array) { -// el.array.sort((a, b) => a > b); -// } else if (el.heap) { -// el.heap.sort(sortMe) -// } else { -// el.func(sortMe); -// } -// -// /******/ var END = window.performance.now(); /******/ -// -// console.log(el.title + ': ' + parseTime(START, END)); -// }); - -// { -// let start = window.performance.now(); -// mergeSort([...BENCHED_ARRAY]); -// let end = window.performance.now(); -// console.log('Mergesort: ' + parseTime(start, end)); // ~10ms -// }{ -// let start = window.performance.now(); -// quickSort([...BENCHED_ARRAY]); -// let end = window.performance.now(); -// console.log('Quicksort: ' + parseTime(start, end)); // ~5ms -// }{ -// let start = window.performance.now(); -// bubbleSort([...BENCHED_ARRAY]); -// let end = window.performance.now(); -// console.log('Bubble Sort: ' + parseTime(start, end)); // ~325ms -// }{ -// let start = window.performance.now(); -// selectionSort([...BENCHED_ARRAY]); -// let end = window.performance.now(); -// console.log('Selection Sort: ' + parseTime(start, end)); // ~20ms -// }{ -// let start = window.performance.now(); -// insertionSort([...BENCHED_ARRAY]); -// let end = window.performance.now(); -// console.log('Insertion Sort: ' + parseTime(start, end)); // ~120ms -// }{ -// const heap = new MinHeap(); -// let start = window.performance.now(); -// heap.sort([...BENCHED_ARRAY]); -// let end = window.performance.now(); -// console.log('Heap Sort: ' + parseTime(start, end)); // ~13ms -// }{ -// // Array.sort is implemented w/ a highly optimized merge sort in most engines -// let start = window.performance.now(); -// [...BENCHED_ARRAY].sort((a, b) => a > b); -// let end = window.performance.now(); -// console.log('Array.sort: ' + parseTime(start, end)); // ~2ms WOW!!! -// } +} diff --git a/src/assets/seed/algorithms/SumAllPrimes.js b/src/assets/seed/algorithms/SumAllPrimes.js index c4982e9..b92e541 100644 --- a/src/assets/seed/algorithms/SumAllPrimes.js +++ b/src/assets/seed/algorithms/SumAllPrimes.js @@ -7,14 +7,14 @@ export default { /** * @function sumAllPrimes * @param {number} num - * @return {number} + * @returns {number} */ function sumAllPrimes(num) { - return num; + return num } -console.log('sumAllPrimes(977) => ' + sumAllPrimes(977)); +console.log('sumAllPrimes(977) => ' + sumAllPrimes(977)) `, solution: `// given a number, return the sum of all prime @@ -23,26 +23,31 @@ console.log('sumAllPrimes(977) => ' + sumAllPrimes(977)); /** * @function sumAllPrimes * @param {number} num - * @return {number} + * @returns {number} */ function sumAllPrimes(num) { - let arr = Array.from({ length: num + 1 }, (v, k) => k).slice(2); + let arr = Array.from({ length: num + 1 }, (v, k) => k).slice(2) let onlyPrimes = arr.filter(n => { - let m = n - 1; + let m = n - 1 while (m > 1 && m >= Math.sqrt(n)) { - if (n % m === 0) return false; - m--; + if (n % m === 0) return false + m-- } - return true; - }); - return onlyPrimes.reduce((a, b) => a + b); + return true + }) + + if (!onlyPrimes.length) { + return 0 + } + + return onlyPrimes.reduce((a, b) => a + b) } -console.log('sumAllPrimes(977) => ' + sumAllPrimes(977)); +console.log('sumAllPrimes(977) => ' + sumAllPrimes(977)) `, resources: [ { href: 'https://www.freecodecamp.org/challenges/sum-all-primes', caption: 'freeCodeCamp Challenge' }, { href: 'https://guide.freecodecamp.org/certificates/sum-all-primes', caption: 'freeCodeCamp Guides (solutions)' }, ] -}; +} diff --git a/src/assets/seed/data-structures/BinarySearchTree.js b/src/assets/seed/data-structures/BinarySearchTree.js index eda3718..2cfcc54 100644 --- a/src/assets/seed/data-structures/BinarySearchTree.js +++ b/src/assets/seed/data-structures/BinarySearchTree.js @@ -3,23 +3,24 @@ export default { seed: `class Node { constructor(value) { - this.value = value; - this.left = null; - this.right = null; + this.value = value + this.left = null + this.right = null } } class BinarySearchTree { constructor() { - this.root = null; + this.root = null } // methods to implement: - // add() + // add(value) + // remove(value) // findMin() // findMax() - // isPresent(int) + // isPresent(value) // findMaxHeight() // findMinHeight() // isBalanced() @@ -28,456 +29,442 @@ class BinarySearchTree { // postOrder() // levelOrder() // reverseLevelOrder() - // remove() // invert() } `, solution: -`// queue helper class node -class QNode { +`/** + * @class Node + * @property value The node's value + * @property left The node's left child + * @property right The node's right child + */ + +class Node { constructor(value) { - this.value = value; - this.next = null; + this.value = value + this.left = null + this.right = null } } -// helper class for levelOrder and reverseLevelOrder methods -class Queue { +/** + * @class BinarySearchTree + * @method add Adds a node to the tree @param {number} + * @method remove @param {number} value @returns {number} Removes and returns the removed element + * @method findMin @returns {number} Returns the smallest value in the tree + * @method findMax @returns {number} Returns the greatest value in the tree + * @method isPresent @param {number} @returns {boolean} Whether or not a value is present in the tree + * @method findMaxHeight @returns {number} Returns the greatest depth (from root to furthest leaf) + * @method findMinHeight @returns {number} Returns the smallest depth (from root to furthest leaf) + * @method isBalanced @returns {boolean} Whether or not the left and right depth difference is <= 1 + * @method inOrder @returns {number[]} An array of the tree's values arranged inOrder + * @method preOrder @returns {number[]} An array of the tree's values arranged in preOrder + * @method postOrder @returns {number[]} An array of the tree's values arranged in postOrder + * @method levelOrder @returns {number[]} An array of the tree's values arranged in levelOrder + * @method reverseLevelOrder @returns {number[]} An array of the tree's values arranged in reverseLevelOrder + * @method invert Inverts the tree in place + */ + +class BinarySearchTree { constructor() { - this.root = null; + this.root = null + this.size = 0 } - enqueue(value) { + + add(value) { if (!this.root) { - this.root = new QNode(value); - return; + this.size++ + this.root = new Node(value) + return } - let node = this.root; - while (node.next) { - node = node.next; + const add = (value, node) => { + if (value > node.value) { + if (!node.right) { + this.size++ + node.right = new Node(value) + return + } else { + return add(value, node.right) + } + } else if (value < node.value) { + if (!node.left) { + this.size++ + node.left = new Node(value) + return + } else { + return add(value, node.left) + } + } + // element already exists + return null } - node.next = new QNode(value); + return add(value, this.root) } - dequeue() { + + remove(value) { if (!this.root) { - return null; + return null } - let value = this.root.value; - this.root = this.root.next; - return value; - } + const { target, parent } = this.__searchTree(value, this.root) - get isEmpty() { - if (!this.root) { - return true; + if (!target) { + return null } - return false; - } -} + // decrement size of list + this.size-- -var q = new Queue(); + // count children + let children = 0 + if (target.right) children++ + if (target.left) children++ -/** - * @class Node - * @property value The node's value - * @property left The node's left child - * @property right The node's right child - */ + // remove leaf node + if (!children) { + if (!parent) { + this.root = null + return + } -class Node { - constructor(value) { - this.value = value; - this.left = null; - this.right = null; - } -} + if (parent.left && parent.left.value === value) { + parent.left = null + } else { + parent.right = null + } + } -/** - * @class BinarySearchTree - * @method add Adds a node to the tree @param {number} - * @method findMin @return {number} Returns the smallest value in the tree - * @method findMax @return {number} Returns the greatest value in the tree - * @method isPresent @param {number} @return {boolean} Whether or not a value is present in the tree - * @method findMaxHeight @return {number} Returns the greatest depth (from root to furthest leaf) - * @method findMinHeight @return {number} Returns the smallest depth (from root to furthest leaf) - * @method isBalanced @return {boolean} Whether or not the left and right depth difference is <= 1 - * @method preOrder @return {number[]} An array of the tree's values arranged in preOrder - * @method postOrder @return {number[]} An array of the tree's values arranged in postOrder - * @method levelOrder @return {number[]} An array of the tree's values arranged in levelOrder - * @method reverseLevelOrder @return {number[]} An array of the tree's values arranged in reverseLevelOrder - * @method remove @param {number} value @return {number} Removes and returns the removed element - * @method invert Inverts the tree in place - */ + // remove node with 1 child + if (children === 1) { + if (!parent) { + if (target.left) { + this.root = target.left + } else { + this.root = target.right + } + return + } -class BinarySearchTree { - constructor() { - this.root = null; - } + if (parent.left && parent.left.value === value) { + if (target.left) { + parent.left = target.left + } else { + parent.left = target.right + } + } else { + if (target.left) { + parent.right = target.left + } else { + parent.right = target.right + } + } + } + // remove node w/ 2 children + if (children === 2) { + if (!parent && target.right && target.left) { + this.root.value = target.right.value + target.right = null + return + } - add(int, node = this.root) { - if (!this.root) { - this.root = new Node(int); - return; - } + var findMin = (minRight, minRightParent) => { + if (minRight.left) { + return findMin(minRight.left, minRight) + } - if (int > node.value) { - if (!node.right) { - node.right = new Node(int); - return; - } else { - return this.add(int, node.right); + return { minRight, minRightParent } } - } else if (int < node.value) { - if (!node.left) { - node.left = new Node(int); - return; + + var { minRight, minRightParent } = findMin(target.right, target) + + target.value = minRight.value + + if (!minRight.left && !minRight.right) { + if (minRightParent.left.value === minRight.value) { + minRightParent.left = null + } else { + minRightParent.right = null + } } else { - return this.add(int, node.left); + minRightParent.left = minRight.right } } - - return null; } - findMin(node = this.root) { - if (!node) { - return null; + findMin() { + if (!this.root) { + return null } - if (node.left) { - return this.findMin(node.left); + const findMin = (node) => { + return node.left + ? findMin(node.left) + : node.value } - return node.value; + return findMin(this.root) } - findMax(node = this.root) { - if (!node) { - return null; + findMax() { + if (!this.root) { + return null } - if (node.right) { - return this.findMax(node.right); + const findMax = (node) => { + return node.right + ? findMax(node.right) + : node.value } - return node.value + return findMax(this.root) } - isPresent(int, node = this.root) { - if (!node) { - return false; + isPresent(value) { + if (!this.root) { + return false } - if (int === node.value) { - return true; - } else if (int > node.value && node.right) { - return this.isPresent(int, node.right); - } else if (int < node.value && node.left) { - return this.isPresent(int, node.left); + const isPresent = (value, node) => { + if (value === node.value) { + return true + } else if (value > node.value && node.right) { + return isPresent(value, node.right) + } else if (value < node.value && node.left) { + return isPresent(value, node.left) + } + return false } - return false; + return isPresent(value, this.root) } - findMaxHeight(node = this.root) { - if (!node) { - return -1; - } + findMaxHeight() { + const height = (node) => { + if (!node) { + return -1 + } - var leftHeight = this.findMaxHeight(node.left); - var rightHeight = this.findMaxHeight(node.right); + var leftHeight = height(node.left) + var rightHeight = height(node.right) - if (leftHeight > rightHeight) { - return leftHeight + 1; - } else { - return rightHeight + 1; + return leftHeight > rightHeight + ? leftHeight + 1 + : rightHeight + 1 } + return height(this.root) } - findMinHeight(node = this.root) { - if (!node) { - return -1; - } + findMinHeight() { + const height = (node) => { + if (!node) { + return -1 + } - var leftHeight = this.findMinHeight(node.left); - var rightHeight = this.findMinHeight(node.right); + var leftHeight = height(node.left) + var rightHeight = height(node.right) - if (leftHeight < rightHeight) { - return leftHeight + 1; - } else { - return rightHeight + 1; + return leftHeight < rightHeight + ? leftHeight + 1 + : rightHeight + 1 } + return height(this.root) } isBalanced() { - var maxHeight = this.findMaxHeight(); - var minHeight = this.findMinHeight(); - if (maxHeight - minHeight >= 1) { - return false; - } else { - return true; - } + return this.findMinHeight() > (this.findMaxHeight() - 1) + ? false + : true } - inOrder(node = this.root, list = []) { - if (!node) { - return null; + inOrder() { + if (!this.root) { + return null } - this.inOrder(node.left, list); - list.push(node.value); - this.inOrder(node.right, list); + const traverse = (node, list) => { + if (node) { + traverse(node.left, list) + list.push(node.value) + traverse(node.right, list) + return list + } + } - return list; + return traverse(this.root, []) } - preOrder(node = this.root, list = []) { - if (!node) { - return null; + preOrder() { + if (!this.root) { + return null } - list.push(node.value); - this.preOrder(node.left, list); - this.preOrder(node.right, list); + const traverse = (node, list) => { + if (node) { + list.push(node.value) + traverse(node.left, list) + traverse(node.right, list) + return list + } + } - return list; + return traverse(this.root, []) } - postOrder(node = this.root, list = []) { - if (!node) { - return null; + postOrder() { + if (!this.root) { + return null } - this.postOrder(node.left, list); - this.postOrder(node.right, list); - list.push(node.value); + const traverse = (node, list) => { + if (node) { + traverse(node.left, list) + traverse(node.right, list) + list.push(node.value) + return list + } + } - return list; + return traverse(this.root, []) } levelOrder() { if (!this.root) { - return null; + return null } - const arr = []; - q.enqueue(this.root); + const results = [], q = [] + q.push(this.root) - while (!q.isEmpty) { - let node = q.dequeue(); - arr.push(node.value); + while (q.length) { + let node = q.shift() + results.push(node.value) if (node.left) { - q.enqueue(node.left); + q.push(node.left) } if (node.right) { - q.enqueue(node.right); + q.push(node.right) } } - return arr; + return results } reverseLevelOrder() { if (!this.root) { - return null; + return null } - const arr = []; - q.enqueue(this.root); + const results = [], q = [] + + q.push(this.root) - while (!q.isEmpty) { - let node = q.dequeue(); - arr.push(node.value); + while (q.length) { + let node = q.shift() + results.push(node.value) if (node.right) { - q.enqueue(node.right); + q.push(node.right) } if (node.left) { - q.enqueue(node.left); + q.push(node.left) } } - return arr; + return results } - remove(value) { + invert() { if (!this.root) { - return null; - } - - const { target, parent } = this.searchTree(value, this.root); - - if (!target) { - return null; - } - - // count children - let children = 0; - if (target.right) children++; - if (target.left) children++; - - // remove leaf node - if (!children) { - if (!parent) { - this.root = null; - return; - } - - if (parent.left && parent.left.value === value) { - parent.left = null; - } else { - parent.right = null; - } + return null } - // remove node with 1 child - if (children === 1) { - if (!parent) { - if (target.left) { - this.root = target.left; - } else { - this.root = target.right; - } - return; - } + const invert = (node) => { + if (node) { + var tempNode = node.left + node.left = node.right + node.right = tempNode - if (parent.left && parent.left.value === value) { - if (target.left) { - parent.left = target.left; - } else { - parent.left = target.right; - } - } else { - if (target.left) { - parent.right = target.left; - } else { - parent.right = target.right; - } + invert(node.left) + invert(node.right) } } - // remove node w/ 2 children - if (children === 2) { - if (!parent && target.right && target.left && this.findMaxHeight() === 1) { - this.root.value = target.right.value; - target.right = null; - return; - } - - var findMin = (minRight, minRightParent) => { - if (minRight.left) { - return findMin(minRight.left, minRight); - } - - return { minRight, minRightParent }; - }; - - var { minRight, minRightParent } = findMin(target.right, target); - - target.value = minRight.value; - - if (!minRight.left && !minRight.right) { - if (minRightParent.left.value === minRight.value) { - minRightParent.left = null; - } else { - minRightParent.right = null; - } - } else { - minRightParent.left = minRight.right; - } - } + invert(this.root) } - // helper method for deletion actions // tracks matching node and parent node - searchTree(value, node, parent) { + __searchTree(value, node, parent) { if (value === node.value) { return { target: node, parent - }; + } } else if (value < node.value && node.left) { - return this.searchTree(value, node.left, node); + return this.__searchTree(value, node.left, node) } else if (value > node.value && node.right) { - return this.searchTree(value, node.right, node); + return this.__searchTree(value, node.right, node) } return { target: null, parent: null - }; - } - - - invert(node = this.root) { - if (!node) { - return null; } - - var tempNode = node.left; - node.left = node.right; - node.right = tempNode; - - this.invert(node.left); - this.invert(node.right); } } -var tree = new BinarySearchTree(); +var tree = new BinarySearchTree() -/* - * Tests - */ +// example usage - const vals = [20,9,49,5,23,52,15,50,17,18,16,13,10,11,12]; - vals.forEach(value => tree.add(value)); +const vals = [20,9,49,5,23,52,15,50,17,18,16,13,10,11,12] +vals.forEach(value => tree.add(value)) -console.log(\`findMax: \${tree.findMax()}\`); -console.log(\`findMin: \${tree.findMin()}\`); -console.log(\`isPresent: \${tree.isPresent(47)}\`); -console.log(\`isPresent: \${tree.isPresent(4)}\`); -console.log(\`maxHeight: \${tree.findMaxHeight()}\`); -console.log(\`minHeight: \${tree.findMinHeight()}\`); -console.log(\`isBalanced: \${tree.isBalanced()}\`); -console.log(\`inorder: \${JSON.stringify(tree.inOrder())}\`); -console.log(\`preorder: \${JSON.stringify(tree.preOrder())}\`); -console.log(\`postorder: \${JSON.stringify(tree.postOrder())}\`); -console.log(\`levelOrder: \${JSON.stringify(tree.levelOrder())}\`); -console.log(\`reverseLevelOrder: \${JSON.stringify(tree.reverseLevelOrder())}\`); +console.log(\`findMax: \${tree.findMax()}\`) +console.log(\`findMin: \${tree.findMin()}\`) +console.log(\`isPresent: \${tree.isPresent(47)}\`) +console.log(\`isPresent: \${tree.isPresent(4)}\`) +console.log(\`maxHeight: \${tree.findMaxHeight()}\`) +console.log(\`minHeight: \${tree.findMinHeight()}\`) +console.log(\`isBalanced: \${tree.isBalanced()}\`) +console.log(\`inorder: \${JSON.stringify(tree.inOrder())}\`) +console.log(\`preorder: \${JSON.stringify(tree.preOrder())}\`) +console.log(\`postorder: \${JSON.stringify(tree.postOrder())}\`) +console.log(\`levelOrder: \${JSON.stringify(tree.levelOrder())}\`) +console.log(\`reverseLevelOrder: \${JSON.stringify(tree.reverseLevelOrder())}\`) console.log('\\nbefore deletion:\\n') -console.log(JSON.stringify(tree, null, 2)); +console.log(JSON.stringify(tree, null, 2)) -tree.remove(50); // remove leaf node -tree.remove(13); // remove node w/ one child -tree.remove(9); // remove node w/ two children +tree.remove(50) // remove leaf node +tree.remove(13) // remove node w/ one child +tree.remove(9) // remove node w/ two children -tree.invert(); +tree.invert() -console.log('\\nafter deletion and inversion:\\n'); -console.log(JSON.stringify(tree, null, 2)); +console.log('\\nafter deletion and inversion:\\n') +console.log(JSON.stringify(tree, null, 2)) `, resources: [ { href: 'http://www.geeksforgeeks.org/binary-search-tree-data-structure/', caption: 'GeeksforGeeks.org'}, @@ -485,8 +472,8 @@ console.log(JSON.stringify(tree, null, 2)); { href: 'https://beta.freecodecamp.org/en/challenges/coding-interview-data-structure-questions/add-a-new-element-to-a-binary-search-tree', caption: 'freeCodeCamp Challenge Series'}, { href: 'https://en.wikipedia.org/wiki/Binary_search_tree', caption: 'Wikipedia'}, { href: 'https://guide.freecodecamp.org/algorithms/binary-search-trees', caption: 'freeCodeCamp Guides'}, - { href: 'https://www.nczonline.net/blog/2009/06/09/computer-science-in-javascript-binary-search-tree-part-1/', caption: 'NCZOnline Blog Pt. 1 (JS Specific)'}, - { href: 'https://www.nczonline.net/blog/2009/06/16/computer-science-in-javascript-binary-search-tree-part-2/', caption: 'NCZOnline Blog Pt. 2 (JS Specific)'}, + { href: 'https://www.nczonline.net/blog/2009/06/09/computer-science-in-javascript-binary-search-tree-part-1/', caption: 'NCZOnline Blog (JS Specific)'}, { href: 'https://www.cs.usfca.edu/~galles/visualization/BST.html', caption: 'Interactive Animated Visualization!'}, + { href: 'https://visualgo.net/en/bst?slide=1', caption: 'VisualAlgo.net: Better Interactive Animated Visualization!'}, ] -}; +} diff --git a/src/assets/seed/data-structures/DoublyLinkedList.js b/src/assets/seed/data-structures/DoublyLinkedList.js index a3f09e1..a1607c4 100644 --- a/src/assets/seed/data-structures/DoublyLinkedList.js +++ b/src/assets/seed/data-structures/DoublyLinkedList.js @@ -3,111 +3,114 @@ export default { seed: `class Node { constructor(value) { - this.data = value; - this.prev = null; - this.next = null; + this.value = value + this.prev = null + this.next = null } } class DoublyLinkedList { constructor() { - this.head = null; - this.tail = null; - this.length = 0; + this.head = null + this.tail = null + this.length = 0 } // methods to implement: // peekHead() // peekTail() - // add() - // addAt() - // remove() - // removeAt() - // indexOf() - // elementAt() + // add(value) + // addAt(index, value) + // remove(value) + // removeAt(index) + // indexOf(value) + // elementAt(index) // isEmpty() + // reverse() // size() } `, solution: `/** - * @class Node - * @property {number|string} value The node's value - * @property {object} prev The previous node - * @property {object} next The next node - */ + * @class Node + * @param {*} element + * @property {*} element The node's value + * @property {object} prev The previous node + * @property {object} next The next node + */ class Node { constructor(value) { - this.data = value; - this.prev = null; - this.next = null; + this.value = value + this.prev = null + this.next = null } } /** * @class Doubly-Linked List data structure - * @property {Object} head Root element of collection - * @property {Object} tail Tail element of collection + * @property {Object} head Root element of list + * @property {Object} tail Tail element of list * @property {number} length The length of the list - * @method peekHead @return {Object} root element of collection - * @method peekTail @return {Object} tail element of collection - * @method add @param {*} element Adds element to List + * @method peekHead @returns {Object} Peek at root element of list + * @method peekTail @returns {Object} Peek at tail element of list + * @method add @param {*} element Appends element to tail of list * @method addAt @param {number} index @param {*} element Adds element at specific index - * @method remove @param {*} element @return {*} removed element or null - * @method removeAt @param {number} index @return {*} removed element at specific index or null - * @method indexOf @param {*} element @return {number} index of a given element or null - * @method elementAt @param {number} index @return {*} elementAt at specific index or null - * @method isEmpty @return {boolean} - * @method size @return size of List + * @method remove @param {*} element @returns {*} Remove and return element from list, return null if no removal + * @method removeAt @param {number} index @returns {*} Remove and return element at specific index, or null if no removal + * @method indexOf @param {*} element @returns {number} Return index of a given element or null if element doesn't exist + * @method elementAt @param {number} index @returns {*} Return element at specific index or null if element doesn't exist + * @method isEmpty @returns {boolean} Return true if list is empty, false if not + * @method reverse Reverses the list structure + * @method size @returns {number} Returns the size of List, can be used interchangably with list.length */ class DoublyLinkedList { constructor() { - this.head = null; - this.tail = null; - this.length = 0; + this.head = null + this.tail = null + this.length = 0 } peekHead() { if (this.isEmpty()) { - return null; + return null } - return this.head; + return this.head } peekTail() { if (this.isEmpty()) { - return null; + return null } - return this.tail; + return this.tail } add(value) { if (this.isEmpty()) { - this.head = new Node(value); - this.tail = this.head; - this.length++; - return; + this.head = new Node(value) + this.tail = this.head + this.length++ + return } - let currentNode = this.head; + let currentNode = this.head while (currentNode.next) { - currentNode = currentNode.next; + currentNode = currentNode.next } - const newNode = new Node(value); - currentNode.next = newNode; - currentNode.next.prev = currentNode; - this.tail = currentNode.next; - this.length++; + const newNode = new Node(value) + currentNode.next = newNode + currentNode.next.prev = currentNode + this.tail = currentNode.next + this.length++ } @@ -115,86 +118,87 @@ class DoublyLinkedList { if (this.isEmpty() || index < 0 || index > this.size) { - return null; + return null } - this.length++; + this.length++ // add at head if (index === 0) { if (!this.head) { - this.head = new Node(value); - this.tail = this.head; - return; + this.head = new Node(value) + this.tail = this.head + return } else { - const newNode = new Node(value); - newNode.next = this.head; - this.head.prev = newNode; - this.head = newNode; - return; + const newNode = new Node(value) + newNode.next = this.head + this.head.prev = newNode + this.head = newNode + return } } // add at tail if (index+1 === this.size) { - const newNode = new Node(value); - this.tail.next = newNode; - newNode.prev = this.tail; - this.tail = newNode; - return; + const newNode = new Node(value) + this.tail.next = newNode + newNode.prev = this.tail + this.tail = newNode + return } - let currentIndex = 0; - let previousNode, currentNode = this.head; + let currentIndex = 0 + let previousNode, currentNode = this.head while (currentIndex !== index) { - previousNode = currentNode; - currentNode = currentNode.next; - currentIndex++; + previousNode = currentNode + currentNode = currentNode.next + currentIndex++ } - const newNode = new Node(value); - previousNode.next = newNode; - newNode.prev = previousNode; - newNode.next = currentNode; - currentNode.prev = newNode; + const newNode = new Node(value) + previousNode.next = newNode + newNode.prev = previousNode + newNode.next = currentNode + currentNode.prev = newNode } remove(value) { if (this.isEmpty()) { - return null; + return null } - this.length--; - // remove head - if (value === this.head.data) { - this.head = this.head.next; - this.head.prev = null; - return value; + if (value === this.head.value) { + this.head = this.head.next + if ( this.head) this.head.prev = null + if (!this.head) this.tail = null + this.length-- + return true } // remove tail - if (value === this.tail.data) { - this.tail = this.tail.prev; - this.tail.next = null; - return value; + if (value === this.tail.value) { + this.tail = this.tail.prev + this.tail.next = null + this.length-- + return true } - let currentNode = this.head; + let currentNode = this.head - while (currentNode.data !== value) { + while (currentNode.value !== value) { if (!currentNode.next) { - return null; + return null } - currentNode = currentNode.next; + currentNode = currentNode.next } - currentNode.prev.next = currentNode.next; - currentNode.next.prev = currentNode.prev; - - return value; + this.length-- + currentNode.prev.next = currentNode.next + currentNode.next.prev = currentNode.prev + return true } @@ -202,57 +206,63 @@ class DoublyLinkedList { if (this.isEmpty() || index < 0 || index >= this.size) { - return null; + return null } - this.length--; + this.length-- // remove at head if (index === 0) { - const deleted = this.head.data; - this.head = this.head.next; - this.head.prev = null; - return deleted; + const deleted = this.head.value + // remove last node + if (this.size === 0) { + this.head = null + this.tail = null + return deleted + } + this.head = this.head.next + this.head.prev = null + return deleted } // remove at tail if (index === this.size) { - const deleted = this.tail.data; - this.tail = this.tail.prev; - this.tail.next = null; - return deleted; + const deleted = this.tail.value + this.tail = this.tail.prev + this.tail.next = null + return deleted } - let currentIndex = 0; - let previousNode, currentNode = this.head; + let currentIndex = 0 + let previousNode, currentNode = this.head while (currentIndex !== index) { - previousNode = currentNode; - currentNode = currentNode.next; - currentIndex++; + previousNode = currentNode + currentNode = currentNode.next + currentIndex++ } - previousNode.next = currentNode.next; - currentNode.next.prev = previousNode; - return currentNode.data; + previousNode.next = currentNode.next + currentNode.next.prev = previousNode + return currentNode.value } indexOf(value) { if (this.isEmpty()) { - return null; + return -1 } - let currentNode = this.head; - let currentIndex = 0; - while (value !== currentNode.data) { - currentNode = currentNode.next; - currentIndex++; + let currentNode = this.head + let currentIndex = 0 + while (value !== currentNode.value) { + currentNode = currentNode.next + currentIndex++ if (!currentNode) { - return -1; + return -1 } } - return currentIndex; + return currentIndex } @@ -260,81 +270,81 @@ class DoublyLinkedList { if (this.isEmpty() || index < 0 || index >= this.size) { - return null; + return null } - let currentIndex = 0; - let currentNode = this.head; + let currentIndex = 0 + let currentNode = this.head while (index !== currentIndex) { - currentNode = currentNode.next; - currentIndex++; + currentNode = currentNode.next + currentIndex++ } - return currentNode.data; + return currentNode.value } isEmpty() { if (!this.head) { - return true; + return true } - return false; + return false } reverse() { if (this.isEmpty()) { - return null; + return null } - let currentNode = this.head; + let currentNode = this.head while (currentNode) { - let tempNode = currentNode.next; - currentNode.next = currentNode.prev; - currentNode.prev = tempNode; - currentNode = currentNode.prev; + let tempNode = currentNode.next + currentNode.next = currentNode.prev + currentNode.prev = tempNode + currentNode = currentNode.prev } - let tempNode = this.head; - this.head = this.tail; - this.tail = tempNode; + let tempNode = this.head + this.head = this.tail + this.tail = tempNode } toString() { if (this.isEmpty()) { - return null; + return null } - let result = []; + let result = [] - let currentNode = this.head; + let currentNode = this.head while (currentNode) { - result.push(Object.assign({}, currentNode)); - currentNode = currentNode.next; + result.push(Object.assign({}, currentNode)) + currentNode = currentNode.next } result.forEach(node => { - if (node.prev) node.prev = node.prev.data; - if (node.next) node.next = node.next.data; - }); + if (node.prev) node.prev = node.prev.value + if (node.next) node.next = node.next.value + }) - return JSON.stringify(result, null, 2); + return JSON.stringify(result, null, 2) } get size() { - return this.length; + return this.length } } // example usage: -const list = new DoublyLinkedList(); +const list = new DoublyLinkedList() console.log( \`\\nNote that all print outs of the list are represented as an @@ -343,52 +353,64 @@ simply show the next and previous data, NOT the entire node) so that we can easily see how the list has been modified. We cannot stringify an un-simplified doubly linked list due to the circular nature of its previous and next node references! (see bottom)\\n\` -); - -list.add('foo'); -list.add('bar'); -list.add('baz'); -list.add('zab'); -list.add('oof'); -list.add('rab'); +) -console.log('\\nsize: ' + list.size); -console.log('initial list: \\n\\n' + list.toString() + '\\n'); +list.add('one') +list.add('two') +list.add('three') +list.add('five') +list.add('six') +list.addAt(3, 'four') -list.remove('foo'); // remove head -list.addAt(0, 'new head'); -list.addAt(4, 'new 4th index'); -list.addAt(7, 'new tail'); +console.log('initial list: \\n\\n' + list.toString() + '\\n') -console.log('\\nsize: ' + list.size); -console.log('modified list: \\n\\n' + list.toString() + '\\n'); +// check node & remove +if (list.elementAt(0) === 'one') { + console.log(list.remove('one')) +} -console.log('\\nremoveAt index 7: ' + list.removeAt(7)); // remove tail -console.log('elementAt index 2: ' + list.elementAt(2)); -console.log('indexOf "new tail": ' + list.indexOf('new tail')); -console.log('indexOf "rab": ' + list.indexOf('rab')); +list.reverse() -list.reverse(); +// loop and remove +while (list.size > 1) { + console.log(\`removed: \${list.removeAt(list.size - 1)} at index: \${list.size-1}\`) +} -console.log('\\nsize: ' + list.size); -console.log('reversed list: \\n\\n' + list.toString() + '\\n'); +console.log('\\n' + list.toString() + '\\n') -// Logging an unmodified doubly linked list would look something like this: -// (notice the [Circular] notation in Node.next.prev) +// remove last node +list.indexOf('six') === 0 && list.removeAt(0) -// In a Node environment: +// removing the last node should reset both head and tail! +console.log('head:', list.head) +console.log('tail:', list.tail) -// Node { -// data: 'rab', -// prev: null, -// next: -// Node { -// data: 'oof', -// prev: [Circular], -// next: Node { data: 'new 4th index', prev: [Object], next: [Object] } } } +/* + * These are very simple exammples. Can you think of some good real world + * use cases for a doubly linked list? How about navigating a playlist? + * A circular doubly linked list could be even more valuable for that! + * Check out the next challenge to see how we can implement one! + */ -// or in a browser environment: -// [object Object] +/* + * NOTE: use the browser's console to log peekHead or peekTail you will get + * a circular structure whose next/prev elements will expand infinitely (since + * they just point at each other) -> Node(A) = Node(A).next.prev = Node(A) + * see an example of this here: http://recordit.co/GT4XT5BVTh + * + * Since logging in a terminal is non-interactive, logging a doubly linked list + * in a Node environment would look something like this (notice the [Circular] + * notation in Node.next.prev): + * + * Node { + * value: 'one', + * prev: null, + * next: + * Node { + * value: 'two', + * prev: [Circular], + * next: Node { value: 'three', prev: [Object], next: [Object] } } } + */ `, resources: [ { href: 'http://www.geeksforgeeks.org/data-structures/linked-list/#doublyLinkedList/', caption: 'GeeksforGeeks.org'}, @@ -396,5 +418,6 @@ console.log('reversed list: \\n\\n' + list.toString() + '\\n'); { href: 'https://beta.freecodecamp.org/en/challenges/coding-interview-data-structure-questions/create-a-doubly-linked-list', caption: 'freeCodeCamp Challenge'}, { href: 'https://en.wikipedia.org/wiki/Doubly_linked_list', caption: 'Wikipedia'}, { href: 'https://guide.freecodecamp.org/computer-science/data-structures/linked-list', caption: 'freeCodeCamp Guides'}, + { href: 'https://visualgo.net/en/list', caption: 'VisualAlgo.net Interactive Animated Visualization'}, ] -}; +} diff --git a/src/assets/seed/data-structures/Graph.js b/src/assets/seed/data-structures/Graph.js index 3d90281..ab8b6f9 100644 --- a/src/assets/seed/data-structures/Graph.js +++ b/src/assets/seed/data-structures/Graph.js @@ -2,83 +2,459 @@ export default { title: 'Graph', seed: `class Graph { - constructor(vertices) { - this.vertices = vertices; - this.list = new Map(); + constructor() { + this.__data__ = new Map() + this.numEdges = 0 } -} // Methods to Implement: // addVertex(vertex) + // removeVertex(vertex) // addEdge(source, destination) - // printGraph() + // removeEdge(source, destination) + // isDirectConnection(vertex, connection) + // isIndirectConnection(vertex, connection) + // getConnections(vertex) + // hasVertex(vertex) + // hasVertices(vertexOne, vertexTwo) + // clear() + // isEmpty() + // size() + // relations() + // pathFromTo(start) + // breadthFirst(start) + // depthFirst(start) + // print() } `, solution: `/** - * @class Graph, Adjacency List Representation - * @property {number} vertices Number of vertices within the graph. - * @method addVertex @param {object} vertex The vertex to be added to the Graph - * @method addEdge @param {object} source @param {object} destination Adds an edge between the source and destination vertices. - * @method printGraph Prints the vertices and their individual adjacency lists + * Class representing a Graph, Adjacency-List representation */ - class Graph { - constructor(vertices) { - this.vertices = vertices; - this.list = new Map(); + /** + * Creates empty Map to store key-value pairs + * + * @property {Map.<(number|string), (number|string)[]>} __data__ + * @property {number} numEdges Number of edges/connections in the Graph + */ + constructor() { + this.__data__ = new Map() + this.numEdges = 0 } + /** + * Adds a vertex to the Graph + * + * @memberOf Graph + * @param {(string|number)} vertex The vertex to be added to the Graph + * @returnss {boolean} Returns true/false if vertex was added + */ addVertex(vertex) { - this.list.set(vertex, []); + return this.__data__.has(vertex) + ? false + : ( + this.__data__.set(vertex, []), + true + ) } - // An undirected graph requires that a connection exists both ways + /** + * Removes a vertex from the Graph + * + * @memberOf Graph + * @param {(string|number)} vertex The vertex to be removed from the Graph + * @returnss {boolean} Returns true/false if vertex was removed + */ + removeVertex(vertex) { + return !this.__data__.has(vertex) + ? false + : ( + // remove map[vertex] + this.__data__.delete(vertex), + // remove associated edges + this.__data__.forEach((v, k, m) => + m.set(k, v.filter(v => v !== vertex)) + ), + true + ) + } + + /** + * Adds an edge/connection between the source & destination vertices + * As an undirected graph, the connection must exist both ways + * + * @memberOf Graph + * @param {(string|number)} source The vertex to add the connection to + * @param {(string|number)} destination The vertex being connected with source + * @returnss {boolean} Returns true/false if connection was successful + */ addEdge(source, destination) { - this.list.get(source).push(destination); - this.list.get(destination).push(source); + return ( + !this.hasVertices(source, destination) || + this.isDirectConnection(source, destination) + ) ? false + : ( + this.numEdges++, + this.__data__.get(source).push(destination), + this.__data__.get(destination).push(source), + true + ) + } + + /** + * Removes an edge/connection between the source & destination vertices + * + * @memberOf Graph + * @param {(string|number)} source The vertex to remove the connection from + * @param {(string|number)} destination The vertex being disconnected from source + * @returnss {boolean} Returns true/false if removal was successful + */ + removeEdge(source, destination) { + return ( + !this.hasVertices(source, destination) || + !this.isDirectConnection(source, destination) + ) ? false + : ( + this.numEdges--, + this.__data__.set( + source, + this.__data__.get(source) + .filter(v => v !== destination) + ), + this.__data__.set( + destination, + this.__data__.get(destination) + .filter(v => v !== source) + ), + true + ) + } + + /** + * Determines if two vertices share an edge/connection + * + * @memberOf Graph + * @param {(string|number)} source The vertex to check + * @param {(string|number)} connection The vertex being checked against source + * @returnss {boolean} Returns true/false if vertices share an edge + */ + isDirectConnection(source, connection) { + return this.hasVertex(source) + ? this.__data__ + .get(source) + .includes(connection) + : false + } + + /** + * Determines if two vertices share an indirect edge/connection + * + * @memberOf Graph + * @param {(string|number)} source The vertex to check + * @param {(string|number)} connection The vertex being checked against source + * @returnss {boolean} Returns true/false if vertices share an indirect edge/connection + */ + isIndirectConnection(source, connection) { + return this.hasVertex(source) + && this.pathFromTo(source, connection) + ? true + : false + } + + /** + * List edges/connections of a given vertex + * + * @memberOf Graph + * @param {(string|number)} vertex The vertex to list connections for + * @returnss {(Array|null)} Returns an array of connections or null if vertex does not exist + */ + getConnections(vertex) { + return this.hasVertex(vertex) + ? this.__data__.get(vertex) + : null + } + + /** + * Determine if the graph has a vertex + * + * @memberOf Graph + * @param {(string|number)} vertex The vertex to check for + * @returnss {boolean} Returns true/false if Graph has vertex + */ + hasVertex(vertex) { + return this.__data__.has(vertex) + ? true + : ( + this.__printMsg(vertex), + false + ) + } + + /** + * Internal/external helper to determine if Graph has 2 vertices + * + * @memberOf Graph + * @param {(string|number)} vertexOne The first vertex to check for + * @param {(string|number)} vertexTwo The second vertex to check for + * @returnss {boolean} Returns true/false if Graph has vertices + */ + hasVertices(vertexOne, vertexTwo) { + return this.hasVertex(vertexOne) + && this.hasVertex(vertexTwo) + ? true + : false + } + + /** + * Clear graph of all data + * + * @memberOf Graph + */ + clear() { + this.__data__.clear() + this.numEdges = 0 + } + + /** + * Helper to determine if Graph is empty + * + * @memberOf Graph + * @returnss {boolean} Returns true/false if Graph is empty + */ + isEmpty() { + return ![ + ...this.__data__.keys() + ].length + } + + /** + * Helper to determine size of Graph + * + * @memberOf Graph + * @returnss {number} Returns number of vertices in Graph + */ + get size() { + return this.__data__.size + } + + /** + * Helper to determine number of edges/connections in Graph + * + * @memberOf Graph + * @returnss {number} Returns number of edges in Graph + */ + get relations() { + return this.numEdges + } + + /** + * Determines the shortest resolvable path between two vertices + * + * @memberOf Graph + * @param {(string|number)} from The vertex to begin traversal from + * @param {(string|number)} to The vertex to traverse to + * @returnss {string} Returns a string representing the shortest resolvable path, e.g. 'A -> B -> C' + */ + pathFromTo(source, destination) { + if (!this.hasVertex(source)) + return null + + const queue = [ source ] + const visited = { [source]: true } + const paths = {}, path = [] + + while (queue.length) { + const vertex = queue.shift() + const edges = this.__data__.get(vertex) + for (let i in edges) { + if (!visited[edges[i]]) { + visited[edges[i]] = true + paths[edges[i]] = vertex + queue.push(edges[i]) + } + } + } + + // path does not exist + if (!visited[destination]) { + this.__printMsg(source, destination) + return null + } + + // resolve path + for (var j = destination; j != source; j = paths[j]) { + path.push(j) + } + + path.push(j) + return path.reverse().join(' -> ') + } + + + /** + * Explore the Graph using a breadth-first search + * + * @memberOf Graph + * @param {(number|string)} start The vertex to begin traversal from + * @returnss {Array} Returns an array of vertices visited in BF order + */ + breadthFirst(start){ + if (!this.hasVertex(start)) + return null + + const visited = { [start]: true }, + queue = [ start ], + results = [] + + while (queue.length) { + const nextVertex = queue.shift() + const adjList = this.__data__.get(nextVertex) + + results.push(nextVertex) + + for (let el of adjList) { + if (!visited[el]) { + visited[el] = true + queue.push(el) + } + } + + } + + return results } - printGraph() { - var vertices = this.list.keys(); + /** + * Explore the Graph using a depth-first search + * + * @memberOf Graph + * @param {(number|string)} start The vertex to begin traversal from + * @returnss {Array} Returns an array of vertices visited in DF order + */ + depthFirst(start) { + if (!this.hasVertex(start)) + return null - for (var i of vertices) { - var vertexAdjList = this.list.get(i); - var vertexConnections = ""; + const visited = {} - for (var j of vertexAdjList) { - vertexConnections += (j + " "); - } + const traverse = (vertex, results = []) => { - console.log(i + " -> " + vertexConnections); + results.push(vertex) + visited[vertex] = true + const adjList = this.__data__.get(vertex) + + for (let el of adjList) { + if (!visited[el]) { + traverse(el, results) + } + } + + return results } + + return traverse(start) + } + + /** + * Helper for visualizing the Graph + * Prints vertices and their corresponding adjacency lists + * + * @memberOf Graph + */ + print() { + for (let [key, value] of this.__data__) { + console.log(\`\${key} -> \${value.join(', ')}\`) + } + } + + /** + * Internal notification util + * Prints appropriate warning message if vertex/vertices not found + * + * @memberOf Graph + * @param {(string|number)} s The source vertex + * @param {(string|number)} d The destination vertex + */ + __printMsg(s, d) { + return !d + ? console.log(\`Vertex '\${s}' not found\`) + : console.log(\`Path from \${s} to \${d} does not exist\`) } } // Example Usage: -var graph = new Graph(5); -var vertices = ['Rat', 'Ox', 'Tiger', 'Rabbit', 'Dragon', 'Snake']; +var graph = new Graph() +var vertices = ['Rat', 'Ox', 'Tiger', 'Rabbit', 'Dragon', 'Snake'] for (var i = 0; i < vertices.length; i++) { - graph.addVertex(vertices[i]); + graph.addVertex(vertices[i]) } -graph.addEdge('Rat', 'Ox'); -graph.addEdge('Tiger', 'Rabbit'); -graph.addEdge('Rat', 'Dragon'); -graph.addEdge('Snake', 'Tiger'); -graph.addEdge('Tiger', 'Rat'); -graph.addEdge('Dragon', 'Ox'); -graph.addEdge('Rabbit', 'Snake'); +graph.addEdge('Rat', 'Ox') +graph.addEdge('Rat', 'Rabbit') +graph.addEdge('Rat', 'Dragon') +graph.addEdge('Ox', 'Tiger') +graph.addEdge('Rabbit', 'Dragon') +graph.addEdge('Dragon', 'Snake') +graph.addEdge('Dragon', 'Tiger') +graph.addEdge('Tiger', 'Snake') + +graph.print() +console.log() -graph.printGraph(); +graph.addEdge('Rat', 'Rooster') // no connection added +graph.addEdge('Monkey', 'Rat') // no connection added +graph.addEdge('Rat', 'Ox') // dupe connection not added + +console.log('\\nRelations (number of edges):', graph.relations) +console.log('Size (number of vertices):', graph.size) + +if (graph.isIndirectConnection('Snake', 'Rat')) { + console.log('\\nPath from Rat to Snake:', graph.pathFromTo('Rat', 'Snake')) +} + +graph.pathFromTo('Rat', 'Monkey') + +console.log('\\nSearches:') +console.log('Depth First:', graph.depthFirst('Rat')) +console.log('Breadth First:', graph.breadthFirst('Rat')) + +if (graph.isDirectConnection('Rat', 'Dragon') && + graph.isDirectConnection('Dragon', 'Snake') + ) { + graph.removeEdge('Rat', 'Dragon') + graph.removeEdge('Snake', 'Dragon') +} + +console.log('\\nhandle removing non-existing connection:') +console.log('graph.removeEdge(\\'Dragon\\', \\'Ox\\') ===', graph.removeEdge('Dragon', 'Ox')) + +if (graph.isDirectConnection('Tiger', 'Dragon')) { + console.log('\\nTiger\\'s connections:', graph.getConnections('Tiger')) + console.log('Dragon\\'s connections:', graph.getConnections('Dragon')) +} + +if (graph.hasVertex('Rabbit')) { + graph.removeVertex('Rabbit') +} + +console.log('\\nModified Graph:') +graph.print() + +if (!graph.isEmpty()) { + graph.clear() + graph.print() +} `, resources: [ { href: 'http://www.geeksforgeeks.org/graph-and-its-representations/', caption: 'GeeksforGeeks.org'}, { href: 'http://www.geeksforgeeks.org/implementation-graph-javascript/', caption: 'GeeksforGeeks.org JS Implementation'}, - { href: 'https://en.wikipedia.org/wiki/Adjacency_list', caption: 'Wikipedia'}, + { href: 'http://blog.benoitvallon.com/data-structures-in-javascript/the-graph-data-structure/', caption: 'Ben\'s Blog, Article & Code'}, { href: 'https://www.cs.usfca.edu/~galles/visualization/ConnectedComponent.html', caption: 'Interactive Animated Visualization'}, + { href: 'https://visualgo.net/en/dfsbfs', caption: 'VisualAlgo.net: Better Interactive Animated Visualizations!'}, + { href: 'https://en.wikipedia.org/wiki/Adjacency_list', caption: 'Wikipedia'}, ] }; diff --git a/src/assets/seed/data-structures/HashTable.js b/src/assets/seed/data-structures/HashTable.js index 70ed71e..9798a42 100644 --- a/src/assets/seed/data-structures/HashTable.js +++ b/src/assets/seed/data-structures/HashTable.js @@ -3,185 +3,203 @@ export default { seed: `class HashTable { constructor() { - this.collection = {}; + this.collection = {} } // methods to implement: - - // hasher() - // add() - // remove() - // lookup() + // hash(key) + // add(key, value) + // remove(key) + // lookup(key) } `, solution: `/** * @class Hash Table data structure * @property {object} collection - * @method hasher @param {string} str The function that produces our hash keys + * @method hash @param {string} str The function that produces our hash keys * @method add @param {string} key @param {*} value The key value pair to add to the hash table - * @method remove @param {string} key @return {*} Accepts an un-hashed key, removes and returns associated value - * @method lookup @param {string} key @return {*} Accepts an un-hashed key, returns associated value + * @method remove @param {string} key @returns {*} Accepts an un-hashed key, removes and returns associated value + * @method lookup @param {string} key @returns {*} Accepts an un-hashed key, returns associated value */ class HashTable { constructor() { - this.collection = {}; + this.collection = {} } /* we use a naive hashing function to demonstrate the problems that can arise from collision */ - hasher(str) { - let hash = 0; - str = String(str); + hash(str) { + let hash = 0 + str = String(str) for (var i in str) { - hash += str.charCodeAt(i); + hash += str.charCodeAt(i) } - return hash; + return hash } add(key, value) { - const hash = this.hasher(key); - const currentValue = this.collection[hash]; + const hash = this.hash(key) + const currentValue = this.collection[hash] if (!currentValue) { - this.collection[hash] = { key, value }; - return; + this.collection[hash] = { key, value } + return } // handle first instance of collision if (!Array.isArray(currentValue)) { - // prevent duplicate keys + // prevent duplicate keys (see note on line 171) if (key === currentValue.key) { - return null; + return null } - this.collection[hash] = [ currentValue, { key, value } ]; + this.collection[hash] = [ currentValue, { key, value } ] - return; + return } // handle subsequent collisions for (let i in currentValue) { // prevent duplicate keys if (currentValue[i].key === key) { - return null; + return null } } - this.collection[hash] = [ ...currentValue, { key, value } ]; + this.collection[hash] = [ ...currentValue, { key, value } ] } remove(key) { - const hash = this.hasher(key); - const currentValue = this.collection[hash]; + const hash = this.hash(key) + const currentValue = this.collection[hash] if (!currentValue) { - return null; + return null } if (!Array.isArray(currentValue)) { - delete this.collection[hash]; - return currentValue.value; + delete this.collection[hash] + return currentValue.value } // handle collision - let deleted; + let deleted for (let i in currentValue) { if (currentValue[i].key === key) { - deleted = currentValue[i]; - currentValue.splice(i, 1); + deleted = currentValue[i] + currentValue.splice(i, 1) } } // remove bucket if 1 value left if (currentValue.length === 1) { - this.collection[hash] = currentValue[0]; + this.collection[hash] = currentValue[0] } - return deleted.value; + return deleted.value } lookup(key) { - const hash = this.hasher(key); - const currentValue = this.collection[hash]; + const hash = this.hash(key) + const currentValue = this.collection[hash] if (!currentValue) { - return null; + return null } + // only one key/val pair stored at this hash key if (currentValue.key === key) { - return currentValue.value; + return currentValue.value } + // otherwise, collision + // iterate through bucket for match for (let i in currentValue) { if (currentValue[i].key === key) { - return currentValue[i].value; + return currentValue[i].value } } - return null; + return null } print() { - console.log(JSON.stringify(this.collection, null, 2)); + console.log(JSON.stringify(this.collection, null, 2)) } } // example usage: -const table = new HashTable(); +const table = new HashTable() // there are several examples of collision here. // luckily, our Hash Table can handle it! // for example, even though the data is unique, // these key-value pairs produce the same hash key: -table.add('Aidan Smith', '(555) 876-2344'); -table.add('Nadia Mihst', '(555) 934-5288'); +table.add('Aidan Smith', '(555) 876-2344') +table.add('Aidan Smith', '(555) 234-4247') +table.add('Nadia Mihst', '(555) 934-5288') // there are some other tricky examples here too. can you spot them? -table.add('Darin Shultz', '(555) 979-8276'); -table.add('Tyler Tate', '(555) 278-4327'); -table.add('Etta Tyler', '(555) 525-0384'); -table.add('Daisy Harris', '(555) 634-0053'); -table.add('Diana Shmit', '(555) 451-8529'); -table.add('Sayid Shirra', '(555) 232-5978'); -table.add('Thomas Brock', '(555) 244-9832'); +table.add('Darin Shultz', '(555) 979-8276') +table.add('Tyler Tate', '(555) 278-4327') +table.add('Etta Tyler', '(555) 525-0384') +table.add('Daisy Harris', '(555) 634-0053') +table.add('Diana Shmit', '(555) 451-8529') +table.add('Sayid Shirra', '(555) 232-5978') +table.add('Thomas Brock', '(555) 244-9832') -table.print(); +table.print() // this is a simple and efficient lookup, since there is no collision at this key -console.log("\\nlookup 'Thomas Brock': " + table.lookup('Thomas Brock')); +console.log("\\nlookup 'Thomas Brock': " + table.lookup('Thomas Brock')) // this lookup is less efficient than the O(n) average // lookup time that can usually be achieved with hash tables. -console.log("lookup 'Sayid Shirra': " + table.lookup('Sayid Shirra')); +console.log("lookup 'Sayid Shirra': " + table.lookup('Sayid Shirra')) /* since there are other elements that share the same hash this key-value -pair produces, our lookup function must iterate through that bucket of -key-value pairs until it finds a match. this is why a good hashing function -will strive to avoid collision as much as possible - collision defeats the -efficiency that makes hash tables great! can you think of a simple solution -for improving the hashing function to avoid this collision? */ + * pair produces, our lookup function must iterate through that bucket of + * key-value pairs until it finds a match. this is why a good hashing function + * will strive to avoid collision as much as possible - collision defeats the + * efficiency that makes hash tables great! can you think of a simple solution + * for improving the hashing function to avoid this collision? + */ // in cases of removal, our hash table is susceptible to // the same efficiency drawbacks if collision is present: -table.remove('Aidan Smith'); -table.remove('Nadia Mihst'); -table.remove('Darin Shultz'); - -console.log("lookup 'Nadia Mihst': " + table.lookup('Nadia Mihst') + '\\n\\n'); - -table.print(); +table.remove('Aidan Smith') +table.remove('Nadia Mihst') +table.remove('Darin Shultz') + +console.log("lookup 'Nadia Mihst': " + table.lookup('Nadia Mihst') + '\\n\\n') + +table.print() + +/* NOTE FROM LINE 41: + * in a real phone book example, dupe keys would + * need to be handled. It might make more sense + * to use the phone number as the key since they + * are guaranteed to be unique. But then that begs + * the question, why use a hashtable at all and not + * a regular JS object with phone numbers as keys and + * names as values? This would provide constant lookup + * time and be less complicated. As you can see, this + * is just for example purposes, and a real-world hash + * table implementation will have to make sense and be + * justified by your particular needs and use-case. + */ `, resources: [ { href: 'http://www.geeksforgeeks.org/hashing-data-structure/', caption: 'GeeksforGeeks.org'}, @@ -191,5 +209,6 @@ table.print(); { href: 'https://www.cs.usfca.edu/~galles/visualization/OpenHash.html', caption: 'Interactive Animated Visualization 1'}, { href: 'https://www.cs.usfca.edu/~galles/visualization/ClosedHash.html', caption: 'Interactive Animated Visualization 2'}, { href: 'https://www.cs.usfca.edu/~galles/visualization/ClosedHashBucket.html', caption: 'Interactive Animated Visualization 3'}, + { href: 'https://visualgo.net/en/hashtable', caption: 'VisualAlgo.net: Better Interactive Animated Visualization!'}, ] -}; +} diff --git a/src/assets/seed/data-structures/LinkedList.js b/src/assets/seed/data-structures/LinkedList.js index c2917ff..f9e5b9c 100644 --- a/src/assets/seed/data-structures/LinkedList.js +++ b/src/assets/seed/data-structures/LinkedList.js @@ -2,220 +2,247 @@ export default { title: 'Linked List', seed: `class Node { - constructor(element) { - this.element = element; - this.next = null; + constructor(value) { + this.value = value + this.next = null } } class LinkedList { constructor() { - this.length = 0; - this.headNode = null; + this.length = 0 + this.head = null } // methods to implement: - // head() + // peekHead() // size() - // add() - // addAt() - // remove() - // removeAt() - // indexOf() - // elementAt() + // add(value) + // addAt(index) + // remove(value) + // removeAt(index) + // indexOf(value) + // elementAt(index) // isEmpty() } `, solution: `/** * @class Node - * @property {number|string} value The node's value - * @property {object} next The next node + * @property {(number|string)} value The node's value + * @property {?Object.} next The next node */ class Node { - constructor(element) { - this.element = element; - this.next = null; + constructor(value) { + this.value = value + this.next = null } } /** * @class Singly-Linked List data structure - * @property {object} headNode Root element of collection + * @property {?Object.} head Root node of collection * @property {number} length The length of the list - * @method head @return {object} root element of collection - * @method size @return size of List - * @method add @param {number|string} element Adds element to List - * @method addAt @param {number} index @param {number|string} element Adds element at specific index - * @method remove @param {number|string} element @return {number|string} removed element - * @method removeAt @param {number} index @return {number|string} removed element at specific index - * @method indexOf @param {number|string} element @return {number} index of a given element - * @method elementAt @param {number} index @return {number|string} elementAt at specific index - * @method isEmpty @return {boolean} + * @method peekHead @returns {?Object.} root node of collection + * @method size @returns {number} size of List + * @method add @param {(number|string)} value Adds node to List + * @method addAt @param {number} index @param {(number|string)} value Adds node at specific index + * @method remove @param {(number|string)} value @returns {?(number|string)} removed element + * @method removeAt @param {number} index @returns {?(number|string)} removed element at specific index + * @method indexOf @param {(number|string)} value @returns {number} index of a given element + * @method elementAt @param {number} index @returns {?(number|string)} elementAt at specific index + * @method isEmpty @returns {boolean} */ class LinkedList { constructor() { - this.length = 0; - this.headNode = null; + this.length = 0 + this.head = null } - head() { - return this.headNode; + peekHead() { + return this.head } get size() { - return length; + return this.length } - add(element) { - var next = new Node(element); - var currentNode = this.headNode; - if (!currentNode) { - this.headNode = next; + add(value) { + var newNode = new Node(value) + if (!this.head) { + this.head = newNode } else { + var currentNode = this.head while (currentNode.next) { - currentNode = currentNode.next; + currentNode = currentNode.next } - currentNode.next = next; + currentNode.next = newNode } - length++; + this.length++ + return true } - addAt(index, element) { + addAt(index, value) { if (index < 0 || index >= this.size) { - return null; + return null } - var currentNode = this.headNode, previousNode; - var currentIndex = 0; - var next = new Node(element); + var currentNode = this.head, previousNode + var currentIndex = 0 + var next = new Node(value) if (index === 0) { - next.next = this.headNode; - this.headNode = next; + next.next = this.head + this.head = next } else { while (currentIndex < index) { - previousNode = currentNode; - currentNode = currentNode.next; - currentIndex++; + previousNode = currentNode + currentNode = currentNode.next + currentIndex++ } - previousNode.next = next; - next.next = currentNode; - currentNode = next; + previousNode.next = next + next.next = currentNode + currentNode = next } - length++; + this.length++ + return true } - remove(element) { - var currentNode = this.headNode, previousNode; + remove(value) { + if (this.isEmpty()) { + return null + } - if (currentNode.element === element) { - this.headNode = currentNode.next; - } else { - while (currentNode.element !== element) { - previousNode = currentNode; - currentNode = currentNode.next; - } + if (this.head.value === value) { + this.length-- + this.head = this.head.next + return true + } - previousNode.next = currentNode.next; + var currentNode = this.head, previousNode + while (currentNode.value !== value) { + previousNode = currentNode + currentNode = currentNode.next + // no match found + if (!currentNode) { + return null + } } - length--; + this.length-- + previousNode.next = currentNode.next + return true } removeAt(index) { - if (index < 0 || index >= this.size) { - return null; + if (index < 0 || + this.isEmpty() || + index >= this.size) { + return null } - var currentNode = this.headNode, previousNode; - var currentIndex = 0; - length--; - + // remove from head if (index === 0) { - previousNode = this.headNode; - this.headNode = currentNode.next; - return previousNode.element; - } else { - while (currentIndex < index) { - previousNode = currentNode; - currentNode = currentNode.next; - currentIndex++; - } + var removed = this.head.value + this.head = this.head.next + this.length-- + return removed + } + + // remove from body / tail + var currentNode = this.head, + previousNode, + currentIndex = 0 - previousNode.next = currentNode.next; - return currentNode.element; + while (currentIndex < index) { + previousNode = currentNode + currentNode = currentNode.next + currentIndex++ } + + this.length-- + previousNode.next = currentNode.next + return currentNode.value + + /* NOTE: this method could be significantly improved + if a 'tail' were added to this structure. Think about + removing the last item in the list. We have to iterate + all the way to the end, and with long lists this can be + quite time consuming. A direct reference to the last item + could prevent this worst-case with a simple equality check. + I've left it like this for now to illustrate this point, but + feel free to try to implement this on your own! We'll add a + tail to our next structure, the doubly linked list. */ } - indexOf(element) { - var count = 0; - var currentNode = this.headNode; - if (!currentNode) return -1; + indexOf(value) { + var count = 0 + var currentNode = this.head + if (!currentNode) return -1 - while (element !== currentNode.element) { + while (value !== currentNode.value) { if (currentNode.next === null) { - return -1; + return -1 } - currentNode = currentNode.next; - count++; + currentNode = currentNode.next + count++ } - return count; + return count } elementAt(index) { if (index < 0 || index >= this.size) { - return null; + return null } - var currentIndex = 0; - var currentNode = this.headNode; + var currentIndex = 0 + var currentNode = this.head while (currentIndex < index) { - currentNode = currentNode.next; - currentIndex++; + currentNode = currentNode.next + currentIndex++ } - return currentNode.element; + return currentNode.value } isEmpty(num) { - if (!this.headNode) { - return true; + if (!this.head) { + return true } - return false; + return false } } // example usage: -var list = new LinkedList(); +var list = new LinkedList() -list.add('Planes'); -list.add('Trains'); -list.add('Automobiles'); -list.add('Magic Carpets'); -console.log(JSON.stringify(list.head(), null, 2)); -console.log(\`indexOf trains: \${list.indexOf('Trains')}\`); -console.log(\`indexOf trucks: \${list.indexOf('Trucks')}\`); -console.log(\`size: \${list.size}\`); +list.add('Planes') +list.add('Trains') +list.add('Automobiles') +list.add('Magic Carpets') +console.log(JSON.stringify(list.peekHead(), null, 2)) +console.log(\`indexOf trains: \${list.indexOf('Trains')}\`) +console.log(\`indexOf trucks: \${list.indexOf('Trucks')}\`) +console.log(\`size: \${list.size}\`) `, resources: [ { href: 'http://www.geeksforgeeks.org/data-structures/linked-list/', caption: 'GeeksforGeeks.org'}, @@ -223,6 +250,6 @@ console.log(\`size: \${list.size}\`); { href: 'https://beta.freecodecamp.org/en/challenges/coding-interview-data-structure-questions/work-with-nodes-in-a-linked-list', caption: 'freeCodeCamp Challenge Series'}, { href: 'https://en.wikipedia.org/wiki/Linked_list', caption: 'Wikipedia'}, { href: 'https://guide.freecodecamp.org/computer-science/data-structures/linked-lists', caption: 'freeCodeCamp Guides'}, - { href: 'http://www.geeksforgeeks.org/data-structures/linked-list/', caption: 'Interactive Animated Visualization!'}, + { href: 'https://visualgo.net/en/list', caption: 'VisualAlgo.net Interactive Animated Visualization'}, ] -}; +} diff --git a/src/assets/seed/data-structures/MaxHeap.js b/src/assets/seed/data-structures/MaxHeap.js index 9959bf8..d349fee 100644 --- a/src/assets/seed/data-structures/MaxHeap.js +++ b/src/assets/seed/data-structures/MaxHeap.js @@ -3,14 +3,14 @@ export default { seed: `class MaxHeap { constructor() { - this.heap = []; - this.length = 0; + this.heap = [] } // methods to implement - // insert() + // insert(number) // remove() + // sort() // print() // size() } @@ -19,108 +19,112 @@ export default { `/** * @class MaxHeap * @property {number[]} heap The heap's collection - * @property {number} length The heap's length * @method insert {number} node Inserts number according to max heap principle - * @method remove @return {number} Returns the max value of the heap + * @method remove @returns {number} Returns the max value of the heap + * @method sort @returns {number[]} returns the sorted heap * @method print Prints the heap to the console - * @method size @return {number} Returns the size of the heap + * @method size @returns {number} Returns the size of the heap */ class MaxHeap { constructor() { - this.heap = []; - this.length = 0; + this.heap = [] } - insert(node) { - this.heap.push(node); - this.length++; + insert(number) { + this.heap.push(number) - var swap = (nodeIdx) => { + const swap = (nodeIdx) => { - var parentIdx = Math.floor((nodeIdx - 1) / 2); - var parent = this.heap[parentIdx]; + const parentIdx = Math.floor((nodeIdx - 1) / 2) + const parent = this.heap[parentIdx] - if (parent < node) { - this.heap[parentIdx] = node; - this.heap[nodeIdx] = parent; - swap(parentIdx); + if (parent < number) { + this.heap[parentIdx] = number + this.heap[nodeIdx] = parent + swap(parentIdx) } - }; + } - if (this.length > 1) { - return swap(this.length-1); + if (this.heap.length > 1) { + return swap(this.heap.length-1) } } - remove(node = this.heap[0]) { + remove() { if (!this.size) { - return null; + return null } - var max = this.heap.shift(); + const max = this.heap.shift() if (this.size > 1) { - this.heap.unshift(this.heap.pop()); + this.heap.unshift(this.heap.pop()) } - var swap = (nodeIdx) => { - var childIdx; + const swap = (node, nodeIdx = 0) => { + let childIdx if (this.size === 2) { - childIdx = 1; + childIdx = 1 } else if (this.heap[2 * nodeIdx + 1] > this.heap[2 * nodeIdx + 2]) { - childIdx = 2 * nodeIdx + 1; + childIdx = 2 * nodeIdx + 1 } else { - childIdx = 2 * nodeIdx + 2; + childIdx = 2 * nodeIdx + 2 } if (node < this.heap[childIdx]) { - this.heap[nodeIdx] = this.heap[childIdx]; - this.heap[childIdx] = node; - return swap(childIdx); + this.heap[nodeIdx] = this.heap[childIdx] + this.heap[childIdx] = node + return swap(node, childIdx) } - this.length--; - return max; + return max + } - }; + return swap(this.heap[0]) + } - return swap(0); + + sort() { + var sorted = [] + while (this.size) { + sorted.push(this.remove()) + } + return sorted.reverse() } print() { - console.log(this.heap); + console.log(this.heap) } get size() { - return this.length; + return this.heap.length } } -var heap = new MaxHeap(); - -heap.insert(7); -heap.insert(10); -heap.insert(14); -heap.insert(32); -heap.insert(2); -heap.insert(64); -heap.insert(37); - -heap.print(); -console.log(\`\\nremove \${heap.remove()}\\n\\n\`); -heap.print(); -console.log(\`\\nremove \${heap.remove()}\\n\\n\`); -heap.print(); +const heap = new MaxHeap() + +const nums = [7, 10, 14, 32, 2, 64, 37] + +for (let num of nums) { + heap.insert(num) +} + +heap.print() +console.log(\`\\nremove \${heap.remove()}\\n\`) +heap.print() +console.log(\`\\nremove \${heap.remove()}\\n\`) +heap.print() `, resources: [ { href: 'http://www.geeksforgeeks.org/heap-data-structure/', caption: 'GeeksforGeeks.org'}, { href: 'https://beta.freecodecamp.org/en/challenges/coding-interview-data-structure-questions/insert-an-element-into-a-max-heap', caption: 'freeCodeCamp Challenge'}, { href: 'https://en.wikipedia.org/wiki/Heap_(data_structure)', caption: 'Wikipedia'}, { href: 'https://www.cs.usfca.edu/~galles/visualization/Heap.html', caption: 'Interactive Animated Visualization!'}, + { href: 'https://visualgo.net/en/heap', caption: 'VisualAlgo.net: Better Interactive Animated Visualization!'}, ] -}; +} diff --git a/src/assets/seed/data-structures/PriorityQueue.js b/src/assets/seed/data-structures/PriorityQueue.js index 027f1ca..410e02d 100644 --- a/src/assets/seed/data-structures/PriorityQueue.js +++ b/src/assets/seed/data-structures/PriorityQueue.js @@ -1,173 +1,222 @@ export default { title: 'Priority Queue', seed: -`class PQNode { - constructor(element, priority) { - this.element = element; - this.priority = priority; - this.next = null; +`// Note: there are many ways to implement a priority queue, this implementation is very similar to a linked +// list, except nodes are inserted according to priority and the lowest priority nodes are always dequeued first. +// Other priority queue implementations might use a 2d array with priority/value pairs as the data store. + +class PQNode { + constructor(value, priority) { + this.value = value + this.priority = priority + this.next = null } } class PriorityQueue { constructor() { - this.head = null; - this.tail = null; - this.length = 0; + this.head = null + this.tail = null + this.size = 0 } // methods to implement: - // enqueue() + // enqueue(value, priority) // dequeue() // front() // isEmpty() + // contains(value) + // priorityOf(value) + // elementAt(priority) // print() } `, solution: `/** - * @class Node - * @property element The node's value / data - * @property priority The node's priority - * @property next The next node in the queue - */ + * @class Node + * @property value The node's value / data + * @property priority The node's priority + * @property next The next node in the queue + */ class PQNode { - constructor(element, priority) { - this.element = element; - this.priority = priority; - this.next = null; + constructor(value, priority) { + this.value = value + this.priority = priority + this.next = null } } /** - * @class Queue - * @property {Object} root The root node of the priority queue - * @method enqueue @property {*} element @property {number} priority Enqueues node based on priority - * @method dequeue @return {*} Removes and returns the front node's value (lowest priority node) - * @method front @return {*} Returns but DOES NOT return the front node's value - * @method size @return {number} Returns the queue's size - * @method isEmpty @return {boolean} - */ - - // NOTE: - // lowest priority takes precedence - // equal priorities are dequeued by insertion order + * @class Queue priority queue data structure + * @property {Object} root The root node of the priority queue + * @property {number} size The priority queue's size + * @method enqueue @property {(number|string)} value @property {number} priority Enqueues node based on priority + * @method dequeue @returns {(number|string)} Removes and returns the front node's value (lowest priority node) + * @method front @returns {(number|string)} Returns but DOES NOT return the front node's value + * @method contains @param value {(number|string)} @returns {boolean} Returns true/false if element is present in queue + * @method priorityOf @param value {(number|string)} @returns {(number|string)} Returns priority of the given element + * @method elementAt @param priority {number} @returns {(number|string)} Returns element at the given priority + * @method isEmpty @returns {boolean} + */ + +// NOTE: +// lowest priority takes precedence +// equal priorities are dequeued by insertion order class PriorityQueue { constructor() { - this.head = null; - this.tail = null; - this.length = 0; + this.head = null + this.tail = null + this.size = 0 } - enqueue(element, priority) { - this.length++; + enqueue(value, priority) { + if (typeof priority !== 'number') { + return null + } + this.size++ + + // insert first node if (!this.head) { - this.head = new PQNode(element, priority); - this.tail = this.head; - return; + this.head = new PQNode(value, priority) + this.tail = this.head + return } - let currentNode = this.head; - - if (priority < currentNode.priority) { - const newNode = new PQNode(element, priority); - newNode.next = this.head; - this.head = newNode; - return; + // insert at head + if (priority < this.head.priority) { + const newNode = new PQNode(value, priority) + newNode.next = this.head + this.head = newNode + return } + // insert at tail if (priority > this.tail.priority) { - this.tail.next = new PQNode(element, priority); - this.tail = this.tail.next; - return; + this.tail.next = new PQNode(value, priority) + this.tail = this.tail.next + return } - while (currentNode) { - if (priority >= currentNode.priority && priority < currentNode.next.priority ) { - const newNode = new PQNode(element, priority); - newNode.next = currentNode.next; - currentNode.next = newNode; - return; + // insert in body + const insert = (node) => { + if (priority >= node.priority && priority < node.next.priority) { + const newNode = new PQNode(value, priority) + newNode.next = node.next + node.next = newNode + return } - currentNode = currentNode.next; + return insert(node.next) } + + return insert(this.head) } dequeue() { if (!this.head) { - return null; + return null } - const element = this.head.element; - this.head = this.head.next; - this.length--; + const value = this.head.value + this.head = this.head.next + this.size-- - return element; + return value } front() { if (!this.head) { - return null; + return null } - return this.head.element; + return this.head.value } isEmpty() { if (!this.head) { - return true; + return true } - return false; + return false } - get size() { - return this.length; + contains(value) { + return this.__search(value) ? true : false + } + + + priorityOf(value) { + const isNode = this.__search(value) + return !isNode ? null : isNode.priority + } + + + elementAt(priority) { + if (typeof priority !== 'number') return null + const isNode = this.__search(null, priority) + return !isNode ? null : isNode.value + } + + + // '__' dangle denotes "private"/internal use method + __search(value, priority, node = this.head) { + if (!node) { + return false + } + if (node.value === value || node.priority === priority) { + return node + } + return this.__search(value, priority, node.next) } print() { if (!this.head) { - return true; + return null } - return JSON.stringify(this.head, null, 2); + return JSON.stringify(this.head, null, 2) } } // example usage: -const pQueue = new PriorityQueue(); - -pQueue.enqueue('five', 5); -pQueue.enqueue('ten', 10); -pQueue.enqueue('zero', 0); -pQueue.enqueue('three', 3); -pQueue.enqueue('nine', 9); -pQueue.enqueue('nine-a', 9); -pQueue.enqueue('twenty-four', 24); - -console.log(pQueue.print() + '\\n'); -console.log('size: ' + pQueue.size); -console.log('front: ' + pQueue.front()); -console.log('dequeue: ' + pQueue.dequeue()); -console.log('dequeue: ' + pQueue.dequeue()); -console.log('dequeue: ' + pQueue.dequeue()); -console.log('dequeue: ' + pQueue.dequeue()); -console.log('dequeue: ' + pQueue.dequeue()); -console.log('dequeue: ' + pQueue.dequeue()); -console.log('size: ' + pQueue.size + '\\n'); -console.log(pQueue.print()); +const pQueue = new PriorityQueue() + +pQueue.enqueue('five', 5) +pQueue.enqueue('ten', 10) +pQueue.enqueue('zero', 0) +pQueue.enqueue('three', 3) +pQueue.enqueue('nine', 9) +pQueue.enqueue('nine-a', 9) +pQueue.enqueue('twenty-four', 24) + +console.log(pQueue.print() + '\\n') +console.log('size: ' + pQueue.size) +console.log('front: ' + pQueue.front()) + +console.log('element at priority 3: ' + pQueue.elementAt(3)) +console.log('element at priority 4: ' + pQueue.elementAt(4)) +console.log('priority of \\'five\\': ' + pQueue.priorityOf('five')) +console.log('priority of \\'foo\\': ' + pQueue.priorityOf('foo')) +console.log('contains \\'nine\\': ' + pQueue.contains('nine')) +console.log('contains \\'cool\\': ' + pQueue.contains('cool')) + +while (pQueue.size > 1) { + console.log('dequeue: ' + pQueue.dequeue()) +} + +console.log('size: ' + pQueue.size + '\\n') +console.log(pQueue.print()) `, resources: [ { href: 'http://www.geeksforgeeks.org/priority-queue-set-1-introduction/', caption: 'GeeksforGeeks.org'}, diff --git a/src/assets/seed/data-structures/Queue.js b/src/assets/seed/data-structures/Queue.js index 59919e5..b1c3dd5 100644 --- a/src/assets/seed/data-structures/Queue.js +++ b/src/assets/seed/data-structures/Queue.js @@ -3,23 +3,24 @@ export default { seed: `class Node { constructor(value) { - this.value = value; - this.next = null; + this.value = value + this.next = null } } class Queue { constructor() { - this.root = null; + this.root = null + this.length = 0 } // methods to implement: - // enqueue() + // enqueue(value) // dequeue() // front() // isEmpty() - // get size() + // size() } `, solution: @@ -31,124 +32,128 @@ class Queue { class Node { constructor(value) { - this.value = value; - this.next = null; + this.value = value + this.next = null } } /** * @class Queue * @property {Object} root The root node of the queue - * @method enqueue @param {*} value @param {Object} [node=this.root] - * @method dequeue @return {*} Removes and returns the front node's value - * @method front @return {*} Returns but DOES NOT return the front node's value - * @method isEmpty @return {boolean} - * @method size @return {number} Returns the queue's size + * @property {Object} tail The tail node of the queue + * @property {number} length The length of the queue + * @method enqueue @param {*} value Insert elements into the queue, O(1) + * @method enqueueLinearTime @param {*} value Insert elements into the queue, O(n) + * @method dequeue @returns {*} Removes and returns the front node's value + * @method front @returns {*} Returns but DOES NOT return the front node's value + * @method isEmpty @returns {boolean} + * @method size @returns {number} Returns the queue's length */ class Queue { constructor() { - this.root = null; + this.root = null + this.tail = null + this.length = 0 } - enqueue(value, node = this.root) { - if (!node) { - this.root = new Node(value); - return; - } - - if (node.next) { - return this.enqueue(value, node.next); + /* without a reference to the tail node, + this structure would only have O(1) deletion. + By simply adding a tail, we can efficiently + insert and delete elements from the queue in + constant time. See the below method for how + this looks in linear, O(n) time. */ + enqueue(value) { + const node = new Node(value) + if (!this.root) { + this.root = node + this.tail = node } else { - node.next = new Node(value); + this.tail.next = node + this.tail = this.tail.next } + this.length++ } - enqueueIterative(value, node = this.root) { - if (!node) { - this.root = new Node(value); - return; - } - - while (node.next) { - node = node.next; + /* We must iterate over the list to + enqueue, resulting in O(n) time, a + major disadvantage for large lists */ + enqueueLinearTime(value) { + if (!this.root) { + this.root = new Node(value) + } else { + let node = this.root + while (node.next) { + node = node.next + } + node.next = new Node(value) } - - node.next = new Node(value); + this.length++ } dequeue() { if (!this.root) { - return null; + return null } - const value = this.root.value; - this.root = this.root.next; - - return value; + this.length-- + const value = this.root.value + this.root = this.root.next + return value } front() { if (!this.root) { - return null; + return null } - return this.root.value; + return this.root.value } isEmpty() { if (!this.root) { - return true; + return true } - return false; + return false } get size() { - if (!this.root) { - return 0; - } - - let size = 1; - let node = this.root; - while (node.next) { - node = node.next; - size++; - } - - return size; + return this.length } } -const q = new Queue(); - -console.log(\`size: \${q.size}\`); -console.log(\`isEmpty: \${q.isEmpty()}\`); - -q.enqueue(7); -q.enqueue(10); -q.enqueue(9); -q.enqueue(47); - -console.log(JSON.stringify(q, null, 2)); - -console.log(\`size: \${q.size}\`); -console.log(\`isEmpty: \${q.isEmpty()}\`); -console.log(\`front: \${q.front()}\`); -console.log(\`dequeue: \${q.dequeue()}\`); -console.log(\`dequeue: \${q.dequeue()}\`); -console.log(\`dequeue: \${q.dequeue()}\`); -console.log(\`front: \${q.front()}\`); -console.log(\`dequeue: \${q.dequeue()}\`); -console.log(\`size: \${q.size}\`); -console.log(\`isEmpty: \${q.isEmpty()}\`); -console.log(\`dequeue: \${q.dequeue()}\`); +// example usage: + +const q = new Queue() + +console.log(\`size: \${q.size}\`) +console.log(\`isEmpty: \${q.isEmpty()}\`) + +q.enqueue(7) +q.enqueue(10) +q.enqueue(9) +q.enqueue(47) + +console.log(JSON.stringify(q, null, 2)) + +console.log(\`size: \${q.size}\`) +console.log(\`isEmpty: \${q.isEmpty()}\`) +console.log(\`front: \${q.front()}\`) +console.log(\`dequeue: \${q.dequeue()}\`) +console.log(\`dequeue: \${q.dequeue()}\`) +console.log(\`dequeue: \${q.dequeue()}\`) +console.log(\`front: \${q.front()}\`) +console.log(\`dequeue: \${q.dequeue()}\`) +console.log(\`size: \${q.size}\`) +console.log(\`isEmpty: \${q.isEmpty()}\`) +console.log(\`dequeue: \${q.dequeue()}\`) `, resources: [ { href: 'http://www.geeksforgeeks.org/queue-data-structure/', caption: 'GeeksforGeeks.org'}, @@ -157,5 +162,6 @@ console.log(\`dequeue: \${q.dequeue()}\`); { href: 'https://en.wikipedia.org/wiki/Queue_(abstract_data_type)', caption: 'Wikipedia'}, { href: 'https://guide.freecodecamp.org/computer-science/data-structures/queues', caption: 'freeCodeCamp Guides'}, { href: 'https://www.cs.usfca.edu/~galles/visualization/QueueLL.html', caption: 'Interactive Animated Visualization!'}, + { href: 'https://visualgo.net/en/list', caption: 'VisualAlgo.net: Better Interactive Animated Visualization!'}, ] -}; +} diff --git a/src/assets/seed/data-structures/Stack.js b/src/assets/seed/data-structures/Stack.js index c43d0bd..0b59a98 100644 --- a/src/assets/seed/data-structures/Stack.js +++ b/src/assets/seed/data-structures/Stack.js @@ -3,20 +3,20 @@ export default { seed: `class Node { constructor(value) { - this.value = value; - this.next = null; + this.value = value + this.next = null } } class Stack { constructor() { - this.root = null; - this.size = 0; + this.root = null + this.size = 0 } // methods to implement: - // push() + // push(value) // pop() // peek() // isEmpty() @@ -28,104 +28,108 @@ class Stack { `/** * @class Node * @property {(number|string)} value The node's value - * @property {object} next The next node + * @property {?Object.} next The next node */ class Node { constructor(value) { - this.value = value; - this.next = null; + this.value = value + this.next = null } } /** * @class Stack data structure - * @property {object} root The root of the collection + * @property {?Object.} root The root of the collection * @property {number} size The size of the collection * @method push @param {(number|string)} value Adds an element to the collection - * @method pop Removes an element from the collection - * @method peek Returns the element at the top of the Stack - * @method isEmpty @return bool + * @method pop @returns {(number|string)} Removes the top node from the stack and returns its value + * @method peek @returns {(number|string)} Returns the value of the node at the top of the Stack + * @method isEmpty @returns {boolean} * @method clear Clears the stack * @method print Prints the collection to the console */ class Stack { constructor() { - this.root = null; - this.size = 0; + this.root = null + this.size = 0 } push(value) { - const node = new Node(value); + const node = new Node(value) if (this.root === null) { - this.root = node; + this.root = node } else { - node.next = this.root; - this.root = node; + node.next = this.root + this.root = node } - this.size++; + this.size++ } pop() { - if (this.isEmpty()) { - return; - } + if (this.isEmpty()) return null - const value = this.root.value; - this.root = this.root.next; - this.size--; + const value = this.root.value + this.root = this.root.next + this.size-- - return value; + return value } peek() { - return this.root.value; + return this.root + ? this.root.value + : null } isEmpty() { - return this.size === 0; + return this.size === 0 } clear() { - this.root = null; - this.size = 0; + this.root = null + this.size = 0 + } + + print() { + console.log(JSON.stringify(this, null, 2)) } } // example usage: -const stack = new Stack(); +const stack = new Stack() -console.log('isEmpty: ' + stack.isEmpty()); +console.log('isEmpty: ' + stack.isEmpty()) -stack.push(23); -stack.push(47); -stack.push(95); +stack.push(23) +stack.push(47) +stack.push(95) +console.log('pop: ' + stack.pop()) +console.log('pop: ' + stack.pop()) +console.log('pop: ' + stack.pop()) console.log('pop: ' + stack.pop()); -console.log('pop: ' + stack.pop()); -console.log('pop: ' + stack.pop()); -console.log('pop: ' + stack.pop()); - -[49,27,63,18,11].forEach(num => stack.push(num)); -console.log('peek: ' + stack.peek()); -console.log('size: ' + stack.size); -console.log('isEmpty: ' + stack.isEmpty()); +[49,27,63,18,11] + .forEach(num => stack.push(num)) -console.log('\\n' + JSON.stringify(stack, null, 2)); +console.log('peek: ' + stack.peek()) +console.log('size: ' + stack.size) +console.log('isEmpty: ' + stack.isEmpty() + '\\n') -stack.clear(); - -console.log('\\ncleared:\\n\\n' + JSON.stringify(stack, null, 2)); +stack.print() +stack.clear() +console.log('\\ncleared:\\n\\n') +stack.print() `, resources: [ { href: 'http://www.geeksforgeeks.org/stack-data-structure/', caption: 'GeeksforGeeks.org'}, @@ -134,5 +138,6 @@ console.log('\\ncleared:\\n\\n' + JSON.stringify(stack, null, 2)); { href: 'https://en.wikipedia.org/wiki/Stack_(abstract_data_type)', caption: 'Wikipedia'}, { href: 'https://guide.freecodecamp.org/computer-science/data-structures/stacks', caption: 'freeCodeCamp Guides'}, { href: 'https://www.cs.usfca.edu/~galles/visualization/StackLL.html', caption: 'Interactive Animated Visualization!'}, + { href: 'https://visualgo.net/en/list', caption: 'VisualAlgo.net: Better Interactive Animated Visualization!'}, ] -}; +} diff --git a/src/assets/seed/welcome.js b/src/assets/seed/welcome.js index 59f43ca..fbb8117 100644 --- a/src/assets/seed/welcome.js +++ b/src/assets/seed/welcome.js @@ -31,8 +31,8 @@ to the other, your code will be saved in a couple of ways: - _RESETTING CODE_ To re-initialize the global state of the application (and clear all of your saved code), you -can simply call the \`resetCode()\` function in the editor, and click 'Run Code'. You will be -prompted with a warning once in the in-browser "mock" console, before the code is actually deleted. +can simply call the \`resetState()\` function in the editor, and click 'Run Code'. You will be +prompted with a warning once in the in-browser "mock" console, before the state is actually reset. - _NOT SAVING CODE_ @@ -48,9 +48,13 @@ code, but if you want to clear it at any time, just hit the button! - _SHORTCUT KEYS_ Some basic hotkeys are built in to help you navigate: -__Next Problem:__ | \`CTRL + SHIFT + >\` -__Previous Problem:__ | \`CTRL + SHIFT + <\` -__Run Code:__ | \`CTRL + SHIFT + Enter\` +__Next Problem:__ | \`CMD/CTRL + SHIFT + >/.\` +__Previous Problem:__ | \`CMD/CTRL + SHIFT + anagramPalindrome
is a function' + }, + { + expression: `typeof anagramPalindrome('string') === 'boolean'`, + message: 'anagramPalindrome accepts a string as an argument and returns a boolean' + }, + { + expression: `anagramPalindrome('armdabbmaboobrd') === true && anagramPalindrome('caerrac') === true`, + message: 'anagramPalindrome returns true when the given string can be rearranged to form a palindrome' + }, + { + expression: `anagramPalindrome('armdabsbmaboobrd') === false && anagramPalindrome('caerracs') === false`, + message: 'anagramPalindrome returns false when the given string cannot be rearranged to form a palindrome' + } +]; diff --git a/src/assets/tests/algorithms/BubbleSort.js b/src/assets/tests/algorithms/BubbleSort.js new file mode 100644 index 0000000..361652f --- /dev/null +++ b/src/assets/tests/algorithms/BubbleSort.js @@ -0,0 +1,23 @@ +export const tests = [ + { + expression: `typeof bubbleSort === 'function'`, + message: 'bubbleSort is a function' + }, + { + expression: `Array.isArray(bubbleSort([1, 2, 3])) && JSON.stringify(bubbleSort([1,2,3])) === '[1,2,3]'`, + message: 'bubbleSort accepts and returns an array' + }, + { + expression: `!/\\.sort\\s*\\(.*\\)/.test(bubbleSort.toString())`, + message: 'bubbleSort does not use the built in Array.sort() method' + }, + { + expression: ` + (() => { + return JSON.stringify(bubbleSort([6,9,23,3564,0,4,99,11,25,74,939,35,1,643,3,75])) === '[0,1,3,4,6,9,11,23,25,35,74,75,99,643,939,3564]' && + JSON.stringify(bubbleSort([987654,54,86753,0,-9,233,111,0,12,9,12,33,4])) === '[-9,0,0,4,9,12,12,33,54,111,233,86753,987654]' && + JSON.stringify(bubbleSort([5,9,10,1,0,2,5,3,2])) === '[0,1,2,2,3,5,5,9,10]'; + })()`, + message: 'bubbleSort sorts arrays from least to greatest' + } +]; diff --git a/src/assets/tests/algorithms/BucketSort.js b/src/assets/tests/algorithms/BucketSort.js new file mode 100644 index 0000000..d0f84e5 --- /dev/null +++ b/src/assets/tests/algorithms/BucketSort.js @@ -0,0 +1,29 @@ +export const tests = [ + { + expression: `typeof bucketSort === 'function'`, + message: 'bucketSort is a function' + }, + { + expression: `Array.isArray(bucketSort([0.01,0.02,0.02])) && JSON.stringify(bucketSort([0.01,0.02,0.02])) === '[0.01,0.02,0.02]'`, + message: 'bucketSort accepts and returns an array' + }, + { + expression: `!/\\.sort\\s*\\(.*\\)/.test(bucketSort.toString())`, + message: 'bucketSort does not use the built in Array.sort() method' + }, + { + expression: ` + (() => { + return JSON.stringify(bucketSort([ + 0.77, 0.39, 0.26, 0.33, 0.55, 0.71, + 0.23, 0.88, 0.47, 0.52, 0.72, 0.99, + 0.63, 0.45, 0.21, 0.12, 0.23, 0.94 + ])) === '[0.12,0.21,0.23,0.23,0.26,0.33,0.39,0.45,0.47,0.52,0.55,0.63,0.71,0.72,0.77,0.88,0.94,0.99]' && + JSON.stringify(bucketSort([ + 0.22, 0.01, 0.02, 0.0001, 0.0102, 0.0210, + 0.011, 0.0233, 0.076, 0.088, 0.99, 0.0654 + ])) === '[0.0001,0.01,0.0102,0.011,0.02,0.021,0.0233,0.0654,0.076,0.088,0.22,0.99]'; + })()`, + message: 'bucketSort sorts arrays of floating point numbers from least to greatest' + } +]; diff --git a/src/assets/tests/algorithms/GenerateCheckerboard.js b/src/assets/tests/algorithms/GenerateCheckerboard.js new file mode 100644 index 0000000..02ce3b9 --- /dev/null +++ b/src/assets/tests/algorithms/GenerateCheckerboard.js @@ -0,0 +1,59 @@ +export const tail = ` + const board_1 = generateCheckerboard(8, 8); + const board_2 = generateCheckerboard(16, 16); +`; + +export const tests = [ + { + expression: "typeof generateCheckerboard === 'function'", + message: 'generateCheckerboard is a function' + }, + { + expression: "typeof board_1 === 'string'", + message: 'generateCheckerboard returns a string' + }, + { + expression: "typeof board_1 === 'string' && board_1.match(/#/g).length === 64", + message: 'an 8x8 board has 64 # chars' + }, + { + expression: "typeof board_1 === 'string' && board_1.match(/\\n/g).length === 8", + message: 'an 8x8 board has 8 \\n chars' + }, + { + expression: "typeof board_1 === 'string' && board_1.match(/ /g).length === 64 || board_1.match(/ /g).length === 68", + message: 'an 8x8 board has 64 or 68 spaces' + }, + { + expression: "typeof board_1 === 'string' && board_2.match(/#/g).length === 256", + message: 'a 16x16 board has 256 # chars' + }, + { + expression: "typeof board_1 === 'string' && board_2.match(/\\n/g).length === 16", + message: 'a 16x16 board has 8 \\n chars' + }, + { + expression: "typeof board_1 === 'string' && board_2.match(/ /g).length === 256 || board_2.match(/ /g).length === 264", + message: 'a 16x16 board has between 256 or 264 spaces' + }, + { + expression: ` + (() => { + let isPassing = true; + [board_1, board_2].forEach(board => { + board.split('\\n').forEach((row, i, arr) => { + if (row) { + if (i % 2 === 0) { + isPassing = row[0] === '#'; + } else { + isPassing = row[0] === ' '; + } + } + }); + }); + return isPassing; + })() + `, + message: 'each even row begins with # and each odd row begins with a space' + } +]; diff --git a/src/assets/tests/algorithms/HeapSort.js b/src/assets/tests/algorithms/HeapSort.js new file mode 100644 index 0000000..6ae4ef3 --- /dev/null +++ b/src/assets/tests/algorithms/HeapSort.js @@ -0,0 +1,82 @@ +export const tail = ` +if (typeof new MinHeap() === 'object') { + MinHeap.prototype.__clear__ = function() { + this.heap = [] + return true + } +} + +let __heap__ +const testHooks = { + beforeAll: () => { + __heap__ = new MinHeap() + }, + beforeEach: () => { + __heap__.__clear__() + typeof __heap__.insert === 'function' && + [72,3,19,24,99,45,33,0].forEach(n => __heap__.insert(n)) + }, + afterAll: () => { + __heap__ = null + } +} +` + +export const tests = [ + { + expression: `typeof __heap__ === 'object'`, + message: 'The MinHeap data structure exists' + }, + { + expression: ` + (() => { + __heap__.__clear__() + return __heap__.heap && Array.isArray(__heap__.heap) && __heap__.heap.length === 0; + })()`, + message: 'The MinHeap data structure has a heap property, initialized as an empty array' + }, + { + expression: `typeof __heap__.insert == 'function'`, + message: 'MinHeap has a method called insert: @param {number} number' + }, + { + expression: `JSON.stringify(__heap__.heap) === '[0,3,19,24,99,45,33,72]'`, + message: 'The insert method adds elements according to the min heap property' + }, + { + expression: `typeof __heap__.remove == 'function'`, + message: 'MinHeap has a method called remove' + }, + { + expression: ` + (() => { + if (__heap__.remove() !== 0) return false + if (JSON.stringify(__heap__.heap) !== '[3,24,19,72,99,45,33]') + return false + if (__heap__.remove() !== 3) return false + if (JSON.stringify(__heap__.heap) !== '[19,24,33,72,99,45]') + return false + if (__heap__.remove() !== 19) return false + if (JSON.stringify(__heap__.heap) !== '[24,45,33,72,99]') + return false + if (__heap__.remove() !== 24) return false + if (JSON.stringify(__heap__.heap) !== '[33,45,99,72]') + return false + return true; + })() + `, + message: 'The remove method removes and returns elements according to the min heap property' + }, + { + expression: `__heap__.__clear__() && __heap__.remove() === null`, + message: 'The remove method returns null when called on an empty heap' + }, + { + expression: `typeof __heap__.sort == 'function'`, + message: 'MinHeap has a method called sort' + }, + { + expression: `JSON.stringify(__heap__.sort()) === '[0,3,19,24,33,45,72,99]'`, + message: 'The sort method returns a sorted array (from least to greatest) containing all the elements in the heap' + } +]; diff --git a/src/assets/tests/algorithms/InsertionSort.js b/src/assets/tests/algorithms/InsertionSort.js new file mode 100644 index 0000000..6107ae3 --- /dev/null +++ b/src/assets/tests/algorithms/InsertionSort.js @@ -0,0 +1,23 @@ +export const tests = [ + { + expression: `typeof insertionSort === 'function'`, + message: 'insertionSort is a function' + }, + { + expression: `Array.isArray(insertionSort([1, 2, 3])) && JSON.stringify(insertionSort([1,2,3])) === '[1,2,3]'`, + message: 'insertionSort accepts and returns an array' + }, + { + expression: `!/\\.sort\\s*\\(.*\\)/.test(insertionSort.toString())`, + message: 'insertionSort does not use the built in Array.sort() method' + }, + { + expression: ` + (() => { + return JSON.stringify(insertionSort([6,9,23,3564,0,4,99,11,25,74,939,35,1,643,3,75])) === '[0,1,3,4,6,9,11,23,25,35,74,75,99,643,939,3564]' && + JSON.stringify(insertionSort([987654,54,86753,0,-9,233,111,0,12,9,12,33,4])) === '[-9,0,0,4,9,12,12,33,54,111,233,86753,987654]' && + JSON.stringify(insertionSort([5,9,10,1,0,2,5,3,2])) === '[0,1,2,2,3,5,5,9,10]'; + })()`, + message: 'insertionSort sorts arrays from least to greatest' + } +]; diff --git a/src/assets/tests/algorithms/Mergesort.js b/src/assets/tests/algorithms/Mergesort.js new file mode 100644 index 0000000..1029d00 --- /dev/null +++ b/src/assets/tests/algorithms/Mergesort.js @@ -0,0 +1,23 @@ +export const tests = [ + { + expression: `typeof mergeSort === 'function'`, + message: 'mergeSort is a function' + }, + { + expression: `Array.isArray(mergeSort([1, 2, 3])) && JSON.stringify(mergeSort([1,2,3])) === '[1,2,3]'`, + message: 'mergeSort accepts and returns an array' + }, + { + expression: `!/\\.sort\\s*\\(.*\\)/.test(mergeSort.toString())`, + message: 'mergeSort does not use the built in Array.sort() method' + }, + { + expression: ` + (() => { + return JSON.stringify(mergeSort([6,9,23,3564,0,4,99,11,25,74,939,35,1,643,3,75])) === '[0,1,3,4,6,9,11,23,25,35,74,75,99,643,939,3564]' && + JSON.stringify(mergeSort([987654,54,86753,0,-9,233,111,0,12,9,12,33,4])) === '[-9,0,0,4,9,12,12,33,54,111,233,86753,987654]' && + JSON.stringify(mergeSort([5,9,10,1,0,2,5,3,2])) === '[0,1,2,2,3,5,5,9,10]'; + })()`, + message: 'mergeSort sorts arrays from least to greatest' + } +] diff --git a/src/assets/tests/algorithms/NoTwoConsecutiveChars.js b/src/assets/tests/algorithms/NoTwoConsecutiveChars.js new file mode 100644 index 0000000..eafb63f --- /dev/null +++ b/src/assets/tests/algorithms/NoTwoConsecutiveChars.js @@ -0,0 +1,63 @@ +export const tail = ` + const isValid = (str) => { + for (let i = 1; i < str.length; i++) { + if (str[i-1] === str[i]) { + return false; + } + } + + return true; + } + + const countChars = (str) => { + const charsMap = {}; + for (let char of str) { + charsMap[char] = -~charsMap[char]; + } + + return charsMap; + } + + const compareChars = (map1, map2) => { + const keys1 = Object.keys(map1).sort(); + const keys2 = Object.keys(map2).sort(); + if (JSON.stringify(keys1) !== JSON.stringify(keys2)) { + return false; + } + + for (let key in map1) { + if (map1[key] !== map2[key]) { + return false; + } + } + + return true; + } +`; +export const tests = [ + { + expression: `typeof noTwoConsecutiveChars === 'function'`, + message: 'noTwoConsecutiveChars is a function' + }, + { + expression: ` + (() => { + const TEST_1 = isValid(noTwoConsecutiveChars('aabba')); + const TEST_2 = isValid(noTwoConsecutiveChars('aaaaaaabbbbcc')); + const TEST_3 = isValid(noTwoConsecutiveChars('aaabaaabbbbbbbbbccccbbcbsd')); + const TEST_4 = isValid(noTwoConsecutiveChars('aaabaaabbbbbbbbbbccccbbcbsd')); + const originalCharsMap_1 = countChars('aaabaaabbbbbbbbbbccccbbcbsd'); + const resultCharsMap_1 = countChars(noTwoConsecutiveChars('aaabaaabbbbbbbbbbccccbbcbsd')); + const originalCharsMap_2 = countChars('aaabaaabbbbbbbbbccccbbcbsd'); + const resultCharsMap_2 = countChars(noTwoConsecutiveChars('aaabaaabbbbbbbbbccccbbcbsd')); + const isSameChars_1 = compareChars(originalCharsMap_1, resultCharsMap_1); + const isSameChars_2 = compareChars(originalCharsMap_2, resultCharsMap_2); + return TEST_1 && TEST_2 && TEST_3 && TEST_4 && isSameChars_1 && isSameChars_2; + })()`, + message: 'whenever possible, noTwoConsecutiveChars returns a string that contains all the characters from the original string, rearranged so that no two consecutive characters are the same' + }, + { + expression: `noTwoConsecutiveChars('aaba') === false && noTwoConsecutiveChars('aaabaaabbbbbbbbbbbccccbbcbsd') === false && typeof noTwoConsecutiveChars('aaabaaabbbbbbbbbbccccbbcbsd') === 'string'`, + message: 'noTwoConsecutiveChars returns false when a string with no consecutive chars can\'t be constructed' + }, +]; diff --git a/src/assets/tests/algorithms/Quicksort.js b/src/assets/tests/algorithms/Quicksort.js new file mode 100644 index 0000000..6d2db59 --- /dev/null +++ b/src/assets/tests/algorithms/Quicksort.js @@ -0,0 +1,23 @@ +export const tests = [ + { + expression: `typeof quickSort === 'function'`, + message: 'quickSort is a function' + }, + { + expression: `Array.isArray(quickSort([1, 2, 3])) && JSON.stringify(quickSort([1,2,3])) === '[1,2,3]'`, + message: 'quickSort accepts and returns an array' + }, + { + expression: `!/\\.sort\\s*\\(.*\\)/.test(quickSort.toString())`, + message: 'quickSort does not use the built in Array.sort() method' + }, + { + expression: ` + (() => { + return JSON.stringify(quickSort([6,9,23,3564,0,4,99,11,25,74,939,35,1,643,3,75])) === '[0,1,3,4,6,9,11,23,25,35,74,75,99,643,939,3564]' && + JSON.stringify(quickSort([987654,54,86753,0,-9,233,111,0,12,9,12,33,4])) === '[-9,0,0,4,9,12,12,33,54,111,233,86753,987654]' && + JSON.stringify(quickSort([5,9,10,1,0,2,5,3,2])) === '[0,1,2,2,3,5,5,9,10]'; + })()`, + message: 'quickSort sorts arrays from least to greatest' + } +] diff --git a/src/assets/tests/algorithms/SelectionSort.js b/src/assets/tests/algorithms/SelectionSort.js new file mode 100644 index 0000000..43ae8e7 --- /dev/null +++ b/src/assets/tests/algorithms/SelectionSort.js @@ -0,0 +1,23 @@ +export const tests = [ + { + expression: `typeof selectionSort === 'function'`, + message: 'selectionSort is a function' + }, + { + expression: `Array.isArray(selectionSort([1, 2, 3])) && JSON.stringify(selectionSort([1,2,3])) === '[1,2,3]'`, + message: 'selectionSort accepts and returns an array' + }, + { + expression: `!/\\.sort\\s*\\(.*\\)/.test(selectionSort.toString())`, + message: 'selectionSort does not use the built in Array.sort() method' + }, + { + expression: ` + (() => { + return JSON.stringify(selectionSort([6,9,23,3564,0,4,99,11,25,74,939,35,1,643,3,75])) === '[0,1,3,4,6,9,11,23,25,35,74,75,99,643,939,3564]' && + JSON.stringify(selectionSort([987654,54,86753,0,-9,233,111,0,12,9,12,33,4])) === '[-9,0,0,4,9,12,12,33,54,111,233,86753,987654]' && + JSON.stringify(selectionSort([5,9,10,1,0,2,5,3,2])) === '[0,1,2,2,3,5,5,9,10]'; + })()`, + message: 'selectionSort sorts arrays from least to greatest' + } +] diff --git a/src/assets/tests/algorithms/SumAllPrimes.js b/src/assets/tests/algorithms/SumAllPrimes.js new file mode 100644 index 0000000..d4ee157 --- /dev/null +++ b/src/assets/tests/algorithms/SumAllPrimes.js @@ -0,0 +1,26 @@ +export const tests = [ + { + expression: `typeof sumAllPrimes === 'function'`, + message: 'sumAllPrimes is a function' + }, + { + expression: `typeof sumAllPrimes(5) === 'number'`, + message: 'sumAllPrimes returns a number' + }, + { + expression: `sumAllPrimes(1) === 0 && sumAllPrimes(-11) === 0`, + message: 'sumAllPrimes returns 0 if there are no prime numbers less than the argument provided' + }, + { + expression: `sumAllPrimes(977) === 73156`, + message: `sumAllPrimes(977) returns 73156` + }, + { + expression: `sumAllPrimes(2000) === 277050`, + message: `sumAllPrimes(2000) returns 277050` + }, + { + expression: `sumAllPrimes(3450) === 761455`, + message: `sumAllPrimes(3450) returns 761455` + } +]; diff --git a/src/assets/tests/data-structures/BinarySearchTree.js b/src/assets/tests/data-structures/BinarySearchTree.js new file mode 100644 index 0000000..4be247f --- /dev/null +++ b/src/assets/tests/data-structures/BinarySearchTree.js @@ -0,0 +1,376 @@ +export const tail = ` +if (typeof new BinarySearchTree() === 'object') { + BinarySearchTree.prototype.__isBinarySearchTree__ = function() { + if (this.root === null) { + return null + } else { + var check = true + function checkTree(node) { + if (node.left != null) { + var left = node.left + if (left.value > node.value) { + check = false + } else { + checkTree(left) + } + } + if (node.right != null) { + var right = node.right + if (right.value < node.value) { + check = false + } else { + checkTree(right) + } + } + } + checkTree(this.root) + return check + } + } + BinarySearchTree.prototype.__inOrder__ = function(node = this.root, list = []) { + if (!node) { + return null + } + + this.__inOrder__(node.left, list) + list.push(node.value) + this.__inOrder__(node.right, list) + + return list + } + BinarySearchTree.prototype.__clearTree__ = function() { + this.root = null + return true + } + BinarySearchTree.prototype.__isNodeValid__ = function() { + if (typeof this.root.value === 'undefined' || + typeof this.root.right === 'undefined' || + typeof this.root.left === 'undefined') { + console.log( + 'WARNING: Nodes must have value, left and right properties for tests to work!' + ) + } + } +} + +let __tree__ +const testHooks = { + beforeAll: () => { + __tree__ = new BinarySearchTree() + }, + beforeEach: () => { + __tree__.__clearTree__() + }, + afterAll: () => { + __tree__ = null + } +} +` + +export const tests = [ + { + expression: `typeof __tree__ === 'object'`, + message: 'The BinarySearchTree data structure exists' + }, + { + expression: `__tree__.root === null`, + message: `The BinarySearchTree data structure has a root property which initializes to a value of null` + }, + { + expression: `typeof __tree__.add === 'function'`, + message: 'The binary search tree has a method called add: @param {number} value' + }, + { + expression: ` + (() => { + [4,1,7,87,34,45,73,8] + .forEach(n => __tree__.add(n)) + __tree__.__isNodeValid__() + return (__tree__.__isBinarySearchTree__()) + })() + `, + message: 'The add method adds elements according to the binary search tree rules' + }, + { + expression: ` + (() => { + __tree__.add(4) + return __tree__.add(4) === null + })() + `, + message: 'Adding an element that already exists returns null' + }, + { + expression: `typeof __tree__.findMin === 'function'`, + message: 'The binary search tree has a method called findMin' + }, + { + expression: + `(() => { + [4,1,7,87,34,45,73,8] + .forEach(n => __tree__.add(n)) + return __tree__.findMin() === 1 + })() + `, + message: 'The findMin method returns the minimum value in the binary search tree' + }, + { + expression: `typeof __tree__.findMax === 'function'`, + message: 'The binary search tree has a method called findMax' + }, + { + expression: + `(() => { + [4,1,7,87,34,45,73,8] + .forEach(n => __tree__.add(n)) + return __tree__.findMax() === 87 + })() + `, + message: 'The findMax method returns the maximum value in the binary search tree' + }, + { + expression: + `(() => { + return __tree__.findMin() === null && __tree__.findMax() === null + })() + `, + message: 'The findMin and findMax methods return null for an empty tree' + }, + { + expression: `typeof __tree__.isPresent === 'function'`, + message: 'The binary search tree has a method called isPresent: @param {number} value' + }, + { + expression: ` + (() => { + [4,7,411,452].forEach(n => __tree__.add(n)) + return __tree__.isPresent(452) && __tree__.isPresent(411) && __tree__.isPresent(7) && !__tree__.isPresent(100) + })() + `, + message: 'The isPresent method correctly checks for the presence or absence of elements added to the tree' + }, + { + expression: `__tree__.isPresent(5) === false`, + message: 'isPresent handles cases where the tree is empty' + }, + { + expression: `typeof __tree__.remove === 'function'`, + message: 'The binary search tree has a method called remove: @param {number} value' + }, + { + expression: `__tree__.remove(100) === null`, + message: 'The remove method returns null for an empty tree' + }, + { + expression: ` + (() => { + [5,94,3].forEach(n => __tree__.add(n)) + return (__tree__.remove(100) === null) + })() + `, + message: 'Trying to remove an element that does not exist returns null' + }, + { + expression: ` + (() => { + __tree__.add(500) + __tree__.remove(500) + return (__tree__.__inOrder__() === null) + })() + `, + message: 'If the root node has no children, deleting it sets the root to null' + }, + { + expression: ` + (() => { + [5,3,7,6,10,12].forEach(n => __tree__.add(n)) + ;[3,12,10].forEach(n => __tree__.remove(n)) + return __tree__.__inOrder__().join('') === '567' + })() + `, + message: 'The remove method removes leaf nodes from the tree' + }, + { + expression: ` + (() => { + [-1,3,7,16].forEach(n => __tree__.add(n)) + ;[16,7,3].forEach(n => __tree__.remove(n)) + return __tree__.__inOrder__().join('') === '-1' + })() + `, + message: 'The remove method removes nodes with one child' + }, + { + expression: ` + (() => { + __tree__.add(15) + __tree__.add(27) + __tree__.remove(15) + return __tree__.__inOrder__().join('') === '27' + })() + `, + message: 'Removing the root in a tree with two nodes sets the second to be the root' + }, + { + expression: ` + (() => { + [1,4,3,7,9,11,14,15,19,50] + .forEach(n => __tree__.add(n)) + const removeNum = [9, 11, 14, 19, 3, 50, 15] + for (let num of removeNum) { + __tree__.remove(num) + if (!__tree__.__isBinarySearchTree__()) { + return false + } + } + return __tree__.__inOrder__().join('') === '147' + })() + `, + message: 'The remove method removes nodes with two children while maintaining the binary search tree structure' + }, + { + expression: ` + (() => { + [100,50,300] + .forEach(n => __tree__.add(n)) + __tree__.remove(100) + return __tree__.__inOrder__().join('') === '50300' + })()`, + message: 'The root can be removed on a tree of three nodes' + }, + { + expression: ` + (() => { + [7,1,9,0,3,8,10,2,5,4,6].forEach(n => __tree__.add(n)) + return __tree__.inOrder().join('') === '012345678910' + })() + `, + message: 'The inOrder method returns an array of the node values that result from an inOrder traversal' + }, + { + expression: `__tree__.inOrder() === null`, + message: 'The inOrder method returns null for an empty tree' + }, + { + expression: ` + (() => { + if (isTestDisabled(BinarySearchTree, 'findMinHeight')) { + return 'DISABLED' + } + [4,1,7,87,34,45,73,8] + .forEach(n => __tree__.add(n)) + return __tree__.findMinHeight() === 1 + })() + `, + message: 'The findMinHeight method returns the minimum height of the tree' + }, + { + expression: ` + (() => { + if (isTestDisabled(BinarySearchTree, 'findMaxHeight')) { + return 'DISABLED' + } + [4,1,7,87,34,45,73,8] + .forEach(n => __tree__.add(n)) + return __tree__.findMaxHeight() === 5 + })() + `, + message: 'The findMaxHeight method returns the maximum height of the tree' + }, + { + expression: ` + (() => { + if (isTestDisabled(BinarySearchTree, 'findMaxHeight') && + isTestDisabled(BinarySearchTree, 'findMaxHeight')) { + return 'DISABLED' + } + const minHeight = __tree__.findMaxHeight() === -1 + const maxHeight = __tree__.findMaxHeight() === -1 + return minHeight && maxHeight + })()`, + message: 'The findMaxHeight and findMinHeight methods return a height of -1 when called on an empty tree' + }, + { + expression: ` + (() => { + if (isTestDisabled(BinarySearchTree, 'isBalanced')) { + return 'DISABLED' + } + [50,17,76,9,23,54,14,19,72,12,67] + .forEach(n => __tree__.add(n)) + return __tree__.isBalanced() + })() + `, + message: 'The isBalanced method returns true if the tree is a balanced binary search tree (the tree\'s min height and max height diff is <= 1)' + }, + { + expression: ` + (() => { + if (isTestDisabled(BinarySearchTree, 'preOrder')) { + return 'DISABLED' + } + const TEST_1 = __tree__.preOrder() === null + ;[7,1,9,0,3,8,10,2,5,4,6].forEach(n => __tree__.add(n)) + const TEST_2 = __tree__.preOrder().join('') === '710325469810' + return TEST_1 && TEST_2 + })() + `, + message: 'The preOrder method returns an array of values representing the tree nodes explored in pre order, or null if the tree is empty' + }, + { + expression: ` + (() => { + if (isTestDisabled(BinarySearchTree, 'postOrder')) { + return 'DISABLED' + } + const TEST_1 = __tree__.postOrder() === null + ;[7,1,9,0,3,8,10,2,5,4,6].forEach(n => __tree__.add(n)) + const TEST_2 = __tree__.postOrder().join('') === '024653181097' + return TEST_1 && TEST_2 + })() + `, + message: 'The postOrder method returns an array of values representing the tree nodes explored in post order, or null if the tree is empty' + }, + { + expression: ` + (() => { + if (isTestDisabled(BinarySearchTree, 'levelOrder')) { + return 'DISABLED' + } + const TEST_1 = __tree__.levelOrder() === null + ;[7,1,9,0,3,8,10,2,5,4,6].forEach(n => __tree__.add(n)) + const TEST_2 = __tree__.levelOrder().join('') === '719038102546' + return TEST_1 && TEST_2 + })() + `, + message: 'The levelOrder method returns an array of values representing the tree nodes explored in level order, or null if the tree is empty' + }, + { + expression: ` + (() => { + if (isTestDisabled(BinarySearchTree, 'reverseLevelOrder')) { + return 'DISABLED' + } + const TEST_1 = __tree__.reverseLevelOrder() === null + ;[7,1,9,0,3,8,10,2,5,4,6].forEach(n => __tree__.add(n)) + const TEST_2 = __tree__.reverseLevelOrder().join('') === '791108305264' + return TEST_1 && TEST_2 + })() + `, + message: 'The reverseLevelOrder method returns an array of values representing the tree nodes explored in reverse level order, or null if the tree is empty' + }, + { + expression: ` + (() => { + if (isTestDisabled(BinarySearchTree, 'invert')) { + return 'DISABLED' + } + const TEST_1 = __tree__.invert() === null + ;[4,1,7,87,34,45,73,8].forEach(n => __tree__.add(n)) + __tree__.invert() + return __tree__.__inOrder__().join('') === '877345348741' + })() + `, + message: 'The invert method correctly inverts the tree structure, or returns null if the tree is empty' + } +] diff --git a/src/assets/tests/data-structures/DoublyLinkedList.js b/src/assets/tests/data-structures/DoublyLinkedList.js new file mode 100644 index 0000000..8780f0f --- /dev/null +++ b/src/assets/tests/data-structures/DoublyLinkedList.js @@ -0,0 +1,402 @@ +export const tail = ` +if (typeof new DoublyLinkedList() === 'object') { + DoublyLinkedList.prototype.__print__ = function() { + if (this.head == null) { + return null + } else { + var result = [] + var node = this.head + while (node.next != null) { + result.push(node.value) + node = node.next + } + result.push(node.value) + return result.join('') + } + } + DoublyLinkedList.prototype.__printReverse__ = function() { + if (this.tail == null) { + return null + } else { + var result = [] + var node = this.tail + while (node.prev != null) { + result.push(node.value) + node = node.prev + } + result.push(node.value) + return result.join('') + } + } + DoublyLinkedList.prototype.__clearList__ = function() { + this.head = null + this.tail = null + this.length = 0 + } + DoublyLinkedList.prototype.__isNodeValid__ = function() { + if (typeof this.head.next === 'undefined' || + typeof this.head.prev === 'undefined' || + typeof this.head.value === 'undefined') { + console.log('WARNING: Nodes must have next, prev and value properties for tests to work!') + return null + } + } +} + +let __list__ +const testHooks = { + beforeAll: () => { + __list__ = new DoublyLinkedList() + }, + beforeEach: () => { + __list__.__clearList__() + }, + afterAll: () => { + __list__ = null + } +} +` + +export const tests = [ + { + expression: `typeof __list__ === 'object'`, + message: 'The DoublyLinkedList data structure exists' + }, + { + expression: `__list__.head === null && __list__.tail === null && __list__.length === 0`, + message: 'The DoublyLinkedList data structure has head, tail and length properties, which initialize to null, null and 0, respectively' + }, + { + expression: `typeof __list__.add === 'function'`, + message: 'The DoublyLinkedList class has a method called add: @param {(string|number)} value' + }, + { + expression: ` + (() => { + __list__.add('cat') + __list__.__isNodeValid__() + return __list__.head.value === 'cat' && __list__.tail.value === 'cat' + })()`, + message: 'The add method assigns the first node added to the head and tail properties' + }, + { + expression: ` + (() => { + __list__.add('cat') + __list__.add('dog') + __list__.add('bird') + __list__.add('pig') + return __list__.__print__() === 'catdogbirdpig' && __list__.__printReverse__() === 'pigbirddogcat' && __list__.tail.next === null && __list__.head.prev === null + })()`, + message: 'Additional elements are appended to the list\'s tail, and each node keeps track of both the next and previous nodes' + }, + { + expression: ` + (() => { + __list__.add('cat') + __list__.add('dog') + const TEST_1 = __list__.length + __list__.add('bird') + __list__.add('pig') + const TEST_2 = __list__.length === 4 + return TEST_1 && TEST_2 + })()`, + message: 'The length property of the DoublyLinkedList class increments every time add is called to reflect the number of nodes in the linked list' + }, + { + expression: `typeof __list__.remove === 'function'`, + message: 'The DoublyLinkedList class has a method called remove: @param {(string|number)} value' + }, + { + expression: ` + (() => { + __list__.add('cat') + __list__.add('dog') + __list__.remove('cat') + const TEST_1 = __list__.head.value === 'dog' && __list__.head.prev === null + __list__.remove('dog') + const TEST_2 = __list__.head === null && __list__.tail === null + return TEST_1 && TEST_2 + })()`, + message: 'When the first node is removed, head assumes the value of the removed node\'s next value, and if truthy, has a prev value set to null' + }, + { + expression: ` + (() => { + __list__.add('cat') + __list__.add('dog') + __list__.add('bird') + __list__.remove('bird') + const TEST_1 = __list__.tail.value === 'dog' && + __list__.tail.prev.value === 'cat' && + __list__.tail.next === null + __list__.remove('dog') + const TEST_2 = __list__.head.next === null + __list__.remove('cat') + return TEST_1 && TEST_2 && + __list__.tail === null && + __list__.head === null + })()`, + message: 'The tail node can be removed, when the list has one or more nodes, and references to previous & next nodes are correctly maintained' + }, + { + expression: ` + (() => { + __list__.add('cat') + __list__.add('dog') + __list__.add('bird') + __list__.remove('dog') + return __list__.head.value === 'cat' && + __list__.head.next.value === 'bird' && + __list__.head.next.prev.value === 'cat' + })()`, + message: 'When an element that is neither the head or tail node is removed, the linked list structure, and references to previous & next nodes are maintained' + }, + { + expression: ` + (() => { + __list__.add('cat') + __list__.add('bird') + __list__.add('pig') + __list__.add('cow') + const TEST_1 = __list__.remove('cat') && __list__.length === 3 + const TEST_2 = __list__.remove('pig') && __list__.length === 2 + const TEST_3 = __list__.remove('cow') && __list__.length === 1 + const TEST_4 = __list__.remove('bird') && __list__.length === 0 + return TEST_1 && TEST_2 && TEST_3 && TEST_4 + })()`, + message: 'For every node removed from the list, the remove method returns a truthy value and decrement the length of the list by one' + }, + { + expression: ` + (() => { + const TEST_1 = __list__.remove('cat') === null + __list__.add('dog') + __list__.add('cat') + const TEST_2 = __list__.remove('bird') === null + return TEST_1 && TEST_2 && __list__.length === 2 + })()`, + message: 'If remove is called on an empty list, or finds no matching value to remove, null is returned and the list\'s length property is not mutated' + }, + { + expression: `typeof __list__.removeAt === 'function'`, + message: 'The DoublyLinkedList class has a method called removeAt: @param {number} index' + }, + { + expression: ` + (() => { + __list__.add('cat') + __list__.add('dog') + __list__.add('bird') + __list__.add('fish') + + // remove 'dog' at index 1 second node is bird, and bird.prev is cat + const TEST_1 = __list__.removeAt(1) === 'dog' && + __list__.head.next.value === 'bird' && + __list__.head.next.prev.value === 'cat' + + // remove 'cat' at head new head is bird, bird.prev is null, second node is fish, fish.prev is bird + const TEST_2 = __list__.removeAt(0) === 'cat' && + __list__.head.value === 'bird' && + __list__.head.prev === null && + __list__.head.next.value === 'fish' && + __list__.head.next.prev.value === 'bird' + + // remove 'fish' at index 1 head is bird, bird.next is null, tail is also now bird, bird.prev is null + const TEST_3 = __list__.removeAt(1) === 'fish' && + __list__.head.next === null && + __list__.tail.value === 'bird' && + __list__.tail.prev === null + + // remove 'bird' from head/tail (last node), both head and tail are null + const TEST_4 = __list__.removeAt(0) === 'bird' && + __list__.head === null && + __list__.tail === null + + return TEST_1 && TEST_2 && TEST_3 && TEST_4 + })()`, + message: 'The removeAt method removes and returns the value at the given index, while retaining the linked list structure/references (consider each of the cases outlined in the list.remove(\'val\') tests above)' + }, + { + expression: ` + (() => { + __list__.add('cat') + __list__.add('dog') + __list__.add('kitten') + const TEST_1 = __list__.length === 3 + __list__.removeAt(1) + const TEST_2 = __list__.length === 2 + __list__.removeAt(1) + const TEST_3 = __list__.length === 1 + __list__.removeAt(1) // no change + __list__.removeAt(0) + const TEST_4 = __list__.length === 0 + return TEST_1 && TEST_2 && TEST_3 && TEST_4 + })()`, + message: 'The removeAt method decrements the length of the list by one for every node removed from the list' + }, + { + expression: ` + (() => { + const TEST_1 = __list__.removeAt(0) === null + __list__.add('cat') + const TEST_2 = __list__.removeAt(1) === null + const TEST_3 = __list__.removeAt(5) === null + const TEST_4 = __list__.removeAt(-5) === null + return TEST_1 && TEST_2 && TEST_3 && TEST_4 + })()`, + message: 'The removeAt method returns null if the given index is less than 0, greater than or equal to the length of the list, or if the list is empty' + }, + { + expression: `typeof __list__.addAt === 'function'`, + message: 'The DoublyLinkedList class has a method called addAt: @param {number} index @param {(string|number)} value' + }, + { + expression: ` + (() => { + __list__.add('cat') + __list__.add('dog') + __list__.addAt(1, 'bird') + return __list__.head.value === 'cat' && + __list__.head.next.value === 'bird' && + __list__.head.next.prev.value === 'cat' && + __list__.tail.value === 'dog' && + __list__.tail.prev.value === 'bird' + })()`, + message: 'The addAt method adds the given value to the list at the given index, while maintaining the linked-list structure/references' + }, + { + expression: ` + (() => { + __list__.add('cat') + __list__.addAt(0, 'bird') + return __list__.head.value === 'bird' && + __list__.head.prev === null && + __list__.tail.value === 'cat' && + __list__.tail.prev.value === 'bird' && + __list__.tail.next === null + })()`, + message: 'When the given index is 0, the value passed to addAt becomes the new head node, referencing the rest of the list in its next property' + }, + { + expression: ` + (() => { + const TEST_1 = __list__.addAt(0, 'cat') === null + __list__.add('cat') + __list__.add('dog') + const TEST_2 = __list__.addAt(4, 'cat') === null + const TEST_3 = __list__.addAt(-4, 'cat') === null + return TEST_1 && TEST_2 && TEST_3 + })()`, + message: 'The addAt method returns null if the given index is less than 0, greater than or equal to the length of the list, or if the list is empty' + }, + { + expression: ` + (() => { + __list__.add('cat') + __list__.add('dog') + __list__.addAt(0, 'bird') + __list__.addAt(1, 'fish') + return __list__.length === 4 + })()`, + message: 'The addAt method increments the length of the linked list by one for each new node added to the list' + }, + { + expression: ` + (() => { + if (isTestDisabled(DoublyLinkedList, 'peekHead')) { + return 'DISABLED' + } + __list__.add('cat') + __list__.add('dog') + const peek = __list__.peekHead() + return peek.value === 'cat' && peek.next.value === 'dog' + })()`, + message: 'The peekHead method returns the head property of the DoublyLinkedList structure, so that you can easily and visually inspect the list' + }, + { + expression: ` + (() => { + if (isTestDisabled(DoublyLinkedList, 'peekTail')) { + return 'DISABLED' + } + __list__.add('cat') + __list__.add('dog') + const peek = __list__.peekTail() + return peek.value === 'dog' && peek.prev.value === 'cat' + })()`, + message: 'The peekTail method returns the tail property of the DoublyLinkedList structure, so that you can easily and visually inspect the list' + }, + { + expression: ` + (() => { + if (isTestDisabled(DoublyLinkedList, 'indexOf')) { + return 'DISABLED' + } + const TEST_1 = __list__.indexOf('cat') === -1 + __list__.add('cat') + __list__.add('dog') + __list__.add('bird') + const TEST_2 = __list__.indexOf('bird') === 2 + __list__.add('pig') + __list__.add('cow') + const TEST_3 = __list__.indexOf('cow') === 4 + __list__.remove('dog') + const TEST_4 = __list__.indexOf('bird') === 1 + const TEST_5 = __list__.indexOf('monkey') === -1 + return TEST_1 && TEST_2 && TEST_3 && TEST_4 && TEST_5 + })()`, + message: 'The indexOf method returns the zero-based index of the given element, or -1 if it doesn\'t exist: @param {(string|number)} value' + }, + { + expression: ` + (() => { + if (isTestDisabled(DoublyLinkedList, 'elementAt')) { + return 'DISABLED' + } + __list__.add('cat') + __list__.add('dog') + const TEST_1 = __list__.elementAt(1) === 'dog' + const TEST_2 = __list__.elementAt(0) === 'cat' + __list__.add('pig') + __list__.add('bird') + __list__.add('toad') + const TEST_3 = __list__.elementAt(3) === 'bird' + __list__.remove('bird') + const TEST_4 = __list__.elementAt(3) === 'toad' + return TEST_1 && TEST_2 && TEST_3 && TEST_4 + })()`, + message: 'The elementAt method returns the element at the given index: @param {number} index' + }, + { + expression: ` + (() => { + if (isTestDisabled(DoublyLinkedList, 'elementAt')) { + return 'DISABLED' + } + const TEST_1 = __list__.elementAt(0) === null + __list__.add('cat') + const TEST_2 = __list__.elementAt(1) === null + const TEST_3 = __list__.elementAt(5) === null + const TEST_4 = __list__.elementAt(-5) === null + return TEST_1 && TEST_2 && TEST_3 && TEST_4 + })()`, + message: 'The elementAt method returns null if the given index is less than 0, greater than or equal to the length of the list, or if the list is empty' + }, + { + expression: ` + (() => { + if (isTestDisabled(DoublyLinkedList, 'reverse')) { + return 'DISABLED' + } + __list__.add('cat') + __list__.add('dog') + __list__.add('pig') + __list__.add('bird') + const reverse = __list__.__printReverse__() + __list__.reverse() + return reverse === __list__.__print__() + })()`, + message: 'The reverse method reverses the doubly linked list in place' + }, +] diff --git a/src/assets/tests/data-structures/Graph.js b/src/assets/tests/data-structures/Graph.js new file mode 100644 index 0000000..42ece53 --- /dev/null +++ b/src/assets/tests/data-structures/Graph.js @@ -0,0 +1,381 @@ +export const tail = ` +if (typeof new Graph() === 'object') { + Graph.prototype.__clearGraph__ = function () { + this.__data__.clear() + this.numEdges = 0 + } + Graph.prototype.__entries__ = function() { + return [...this.__data__.entries()] + } +} + +let __graph__ +let oldConsoleLog = console.log + +const testHooks = { + beforeAll: () => { + __graph__ = new Graph() + if (typeof __graph__.__data__ === 'undefined' || + typeof __graph__.numEdges === 'undefined' ) { + console.log( + 'WARNING: Graph must have properties __data__ and numEdges for tests to work!\\n' + ) + } + }, + beforeEach: () => { + console.log = () => {} + __graph__.__clearGraph__() + typeof __graph__.addVertex === 'function' && + ['A', 'B', 'C', 1, 2, 3].forEach(v => __graph__.addVertex(v)) + }, + afterEach: () => { + console.log = oldConsoleLog + }, + afterAll: () => { + __graph__ = null + } +} +` + +export const tests = [ + { + expression: `typeof new Graph() === 'object'`, + message: `The Graph data structure exists` + }, + { + expression: `(() => { + const __newGraph__ = new Graph() + return Object.prototype.toString.call(__newGraph__.__data__) === '[object Map]' && __newGraph__.__data__.size === 0 && __newGraph__.numEdges === 0 + })()`, + message: `The Graph data structure has __data__ and numEdges properties which initialize to a new Map and 0 respectively` + }, + { + expression: `typeof __graph__.addVertex === 'function'`, + message: `The Graph class has an addVertex method: @param {(string|number)} vertex` + }, + { + method: 'deepEqual', + expression: `__graph__.__entries__()`, + expected: [["A",[]],["B",[]],["C",[]],[1,[]],[2,[]],[3,[]]], + message: `The addVertex method adds unique entries to the the Graph's internal Map object; the given vertex as the key, and an empty array (initalized adjacency list) as the value` + }, + { + expression: `typeof __graph__.addEdge === 'function'`, + message: `The Graph class has an addEdge method: @param {(string|number)} source @param {(string|number)} destination` + }, + { + method: 'deepEqual', + expression: `(() => { + __graph__.addEdge(1, 3) + __graph__.addEdge('B', 3) + __graph__.addEdge('A', 'B') + __graph__.addEdge('C', 'A') + return __graph__.__entries__() + })()`, + expected: [["A",["B","C"]],["B",[3,"A"]],["C",["A"]],[1,[3]],[2,[]],[3,[1,"B"]]], + message: `The addEdge method adds an edge, or connection, between two existing vertices by adding the destination vertex to the source vertex's corresponding adjacency list, and vice versa` + }, + { + method: 'deepEqual', + expression: `(() => { + __graph__.addEdge(1, 'B') + const TEST_1 = __graph__.addEdge(1, 'B') === false // duplicate edge + const TEST_2 = __graph__.addEdge(1, 'Hi') === false // second arg is not vertex + const TEST_3 = __graph__.addEdge('Yo', 'B') === false // first arg is not vertex + return TEST_1 && TEST_2 && TEST_3 && __graph__.__entries__() + })()`, + expected: [["A",[]],["B",[1]],["C",[]],[1,["B"]],[2,[]],[3,[]]], + message: `The addEdge method returns false and does not add a connection if an edge between the given vertices already exists, or if either of the given vertices do not exist` + }, + { + expression: `(() => { + __graph__.addEdge(1, 3) + __graph__.addEdge('B', 3) + const TEST_1 = __graph__.numEdges === 2 + __graph__.addEdge('A', 'B') + __graph__.addEdge('C', 'A') + return TEST_1 && __graph__.numEdges === 4 + })()`, + message: `The addEdge method increments the graph's numEdges property by one for each edge added to the graph` + }, + { + expression: `typeof __graph__.removeVertex === 'function'`, + message: `The Graph class has a removeVertex method: @param {(string|number)} vertex` + }, + { + method: 'deepEqual', + expression: `(() => { + __graph__.removeVertex('A') + return __graph__.__entries__() + })()`, + expected: [["B",[]],["C",[]],[1,[]],[2,[]],[3,[]]], + message: `The removeVertex method removes the given vertex from the graph` + }, + { + method: 'deepEqual', + expression: `(() => { + __graph__.addEdge(1, 3) + __graph__.addEdge('B', 3) + __graph__.addEdge('A', 'B') + __graph__.addEdge('C', 'A') + __graph__.removeVertex('A') + return __graph__.__entries__() + })()`, + expected: [["B",[3]],["C",[]],[1,[3]],[2,[]],[3,[1,"B"]]], + message: `The removeVertex method removes any edges/connections associated with the given vertex from the graph` + }, + { + method: 'deepEqual', + expression: `(() => { + return __graph__.removeVertex('J') === false && __graph__.__entries__() + })()`, + expected: [["A",[]],["B",[]],["C",[]],[1,[]],[2,[]],[3,[]]], + message: `The removeVertex method does not mutate the graph and returns false if the given vertex does not exist` + }, + { + expression: `typeof __graph__.removeEdge === 'function'`, + message: `The Graph class has a removeEdge method: @param {(string|number)} source @param {(string|number)} destination` + }, + { + method: 'deepEqual', + expression: `(() => { + __graph__.addEdge(1, 3) + __graph__.addEdge('B', 3) + __graph__.addEdge('A', 'B') + __graph__.addEdge('C', 'A') + __graph__.removeEdge('B', 'A') + __graph__.removeEdge(3, 'B') + __graph__.removeEdge(3, 1) + __graph__.removeEdge('C', 'A') + return __graph__.__entries__() + })()`, + expected: [["A",[]],["B",[]],["C",[]],[1,[]],[2,[]],[3,[]]], + message: `The removeEdge method removes the edge/connection between the given source and destination vertices, and vice versa` + }, + { + expression: `(() => { + __graph__.addEdge(1, 3) + __graph__.addEdge('B', 3) + __graph__.addEdge('A', 'B') + __graph__.addEdge('C', 'A') + __graph__.removeEdge('B', 'A') + __graph__.removeEdge(3, 'B') + const TEST_1 = __graph__.numEdges === 2 + __graph__.removeEdge(3, 1) + __graph__.removeEdge('C', 'A') + const TEST_2 = __graph__.numEdges === 0 + return TEST_1 && TEST_2 + })()`, + message: `The removeEdge method decrements the graph's numEdges property by one for each edge removed from the graph` + }, + { + method: 'deepEqual', + expression: `(() => { + __graph__.addEdge(1, 3) + const TEST_1 = __graph__.removeEdge(1, 'A') === false + const TEST_2 = __graph__.removeEdge('Annoying', 'A') === false + const TEST_3 = __graph__.removeEdge('A', 'Test') === false + const TEST_4 = __graph__.removeEdge('Logs', 'Logs') === false + return TEST_1 && TEST_2 && TEST_3 && TEST_4 && __graph__.__entries__() + })()`, + expected: [["A",[]],["B",[]],["C",[]],[1,[3]],[2,[]],[3,[1]]], + message: `The removeEdge method does not mutate the graph and returns false if the given source and destination vertices do not share an edge or if either of the given vertices do not exist` + }, + { + expression: `typeof __graph__.size === 'function' || typeof __graph__.size === 'number'`, + message: `The Graph class has a size method or property` + }, + { + expression: `(() => { + const TEST_1 = typeof __graph__.size === 'function' + ? __graph__.size() === 6 + : __graph__.size === 6 + __graph__.addVertex('P') + __graph__.addVertex('W') + __graph__.addVertex('P') + __graph__.addVertex('W') + const TEST_2 = typeof __graph__.size === 'function' + ? __graph__.size() === 8 + : __graph__.size === 8 + return TEST_1 && TEST_2 + })()`, + message: `The size method or propery correctly tracks and returns the graph's current size (number of vertices)` + }, + { + expression: `typeof __graph__.relations === 'function' || typeof __graph__.relations === 'number'`, + message: `The Graph class has a relations method or property` + }, + { + expression: `(() => { + __graph__.addEdge(1, 3) + __graph__.addEdge('B', 3) + __graph__.addEdge('A', 'B') + __graph__.addEdge('C', 'A') + const TEST_1 = typeof __graph__.relations === 'function' + ? __graph__.relations() === 4 + : __graph__.relations === 4 + __graph__.addEdge('C', 1) + __graph__.addEdge('C', 2) + __graph__.addEdge('C', 1) + __graph__.addEdge('C', 2) + const TEST_2 = typeof __graph__.relations === 'function' + ? __graph__.relations() === 6 + : __graph__.relations === 6 + return TEST_1 && TEST_2 + })()`, + message: `The relations method or propery correctly tracks and returns the graph's current number of relations (number of edges/connections)` + }, + { + expression: `typeof __graph__.depthFirst === 'function'`, + message: `The Graph class has a depthFirst search method: @param {(string|number)} startingVertex` + }, + { + expression: `typeof __graph__.breadthFirst === 'function'`, + message: `The Graph class has a breadthFirst search method: @param {(string|number)} startingVertex` + }, + { + expression: `(() => { + __graph__.__clearGraph__() + ;[0,1,2,3,4,5].forEach(v => __graph__.addVertex(v)) + const edges = [[0, 1],[0, 3],[0, 4],[1, 2],[3, 4],[4, 5],[4, 2],[2, 5]] + for (let [s, d] of edges) { + __graph__.addEdge(s, d) + } + // depthFirst(0) => 0 -> 1 -> 2 -> 4 -> 3 -> 5 + const TEST_1 = /\\D?0\\D*1\\D*2\\D*4\\D*3\\D*5\\D?/.test(__graph__.depthFirst(0)) + // depthFirst(1) => 1 -> 0 -> 3 -> 4 -> 5 -> 2 + const TEST_2 = /\\D?1\\D*0\\D*3\\D*4\\D*5\\D*2\\D?/.test(__graph__.depthFirst(1)) + // depthFirst(3) => 3 -> 0 -> 1 -> 2 -> 4 -> 5 + const TEST_3 = /\\D?3\\D*0\\D*1\\D*2\\D*4\\D*5\\D?/.test(__graph__.depthFirst(3)) + // depthFirst(5) => 5 -> 4 -> 0 -> 1 -> 2 -> 3 + const TEST_4 = /\\D?5\\D*4\\D*0\\D*1\\D*2\\D*3\\D?/.test(__graph__.depthFirst(5)) + return TEST_1 && TEST_2 && TEST_3 && TEST_4 + })()`, + message: `The depthFirst method returns an array of values or a string representing the vertices of the graph explored from the given startingVertex in depth first order` + }, + { + expression: `(() => { + __graph__.__clearGraph__() + ;[0,1,2,3,4,5].forEach(v => __graph__.addVertex(v)) + const edges = [[0, 1],[0, 3],[0, 4],[1, 2],[3, 4],[4, 5],[4, 2],[2, 5]] + for (let [s, d] of edges) { + __graph__.addEdge(s, d) + } + // breadthFirst(0) => 0 -> 1 -> 3 -> 4 -> 2 -> 5 + const TEST_1 = /\\D?0\\D*1\\D*3\\D*4\\D*2\\D*5\\D?/.test(__graph__.breadthFirst(0)) + // breadthFirst(1) => 1 -> 0 -> 2 -> 3 -> 4 -> 5 + const TEST_2 = /\\D?1\\D*0\\D*2\\D*3\\D*4\\D*5\\D?/.test(__graph__.breadthFirst(1)) + // breadthFirst(3) => 3 -> 0 -> 4 -> 1 -> 5 -> 2 + const TEST_3 = /\\D?3\\D*0\\D*4\\D*1\\D*5\\D*2\\D?/.test(__graph__.breadthFirst(3)) + // breadthFirst(5) => 5 -> 4 -> 2 -> 0 -> 3 -> 1 + const TEST_4 = /\\D?5\\D*4\\D*2\\D*0\\D*3\\D*1\\D?/.test(__graph__.breadthFirst(5)) + return TEST_1 && TEST_2 && TEST_3 && TEST_4 + })()`, + message: `The breadthFirst method returns an array of values or a string representing the vertices of the graph explored from the given startingVertex in breadth first order` + }, + { + expression: `(() => { + if (isTestDisabled(Graph, 'pathFromTo')) return 'DISABLED' + __graph__.__clearGraph__() + ;[0,1,2,3,4,5,6,7,8,9,10].forEach(v => __graph__.addVertex(v)) + const edges = [[0, 1],[0, 3],[0, 4],[3, 4],[4, 5],[4, 2],[2, 5],[7, 6],[8, 7],[2, 6],[2, 6],[9, 10]] + for (let [s, d] of edges) { + __graph__.addEdge(s, d) + } + // pathFromTo(3, 8) => 3 -> 4 -> 2 -> 6 -> 7 -> 8 + const TEST_1 = /\\D?3\\D*4\\D*2\\D*6\\D*7\\D*8\\D?/.test(__graph__.pathFromTo(3, 8)) + // pathFromTo(1, 5) => 1 -> 0 -> 4 -> 5 + const TEST_2 = /\\D?1\\D*0\\D*4\\D*5\\D?/.test(__graph__.pathFromTo(1, 5)) + return TEST_1 && TEST_2 + })()`, + message: `The Graph class has a pathFromTo method which returns a string or an array of values representing the shortest path between two given vertices: @param {(string|number)} fromVertex @param {(string|number)} toVertex` + }, + { + expression: `(() => { + if (isTestDisabled(Graph, 'pathFromTo')) return 'DISABLED' + __graph__.__clearGraph__() + ;[0,9].forEach(v => __graph__.addVertex(v)) + return __graph__.pathFromTo(0, 9) === null && __graph__.pathFromTo(11, 2) === null + })()`, + message: `The pathFromTo method returns null if a path does not exist between the given vertices, or if the given fromVertex does not exist` + }, + { + expression: `(() => { + if (isTestDisabled(Graph, 'isDirectConnection')) return 'DISABLED' + __graph__.addEdge(1, 3) + const TEST_1 = __graph__.isDirectConnection(1, 3) === true + const TEST_2 = __graph__.isDirectConnection(1, 'A') === false + return TEST_1 && TEST_2 + })()`, + message: `The isDirectConnection method returns true if the given vertices share an edge, otherwise false: @param {(string|number)} source @param {(string|number)} connection` + }, + { + expression: `(() => { + if (isTestDisabled(Graph, 'isIndirectConnection')) return 'DISABLED' + __graph__.addEdge(1, 3) + __graph__.addEdge(3, 'A') + const TEST_1 = __graph__.isIndirectConnection(1, 'A') === true + const TEST_2 = __graph__.isIndirectConnection(1, 2) === false + return TEST_1 && TEST_2 + })()`, + message: `The isIndirectConnection method returns true if the given vertices share an indirect connection, otherwise false: @param {(string|number)} source @param {(string|number)} connection` + }, + { + expression: `(() => { + if (isTestDisabled(Graph, 'getConnections')) return 'DISABLED' + __graph__.__clearGraph__() + ;[0,1,2,3,4].forEach(v => __graph__.addVertex(v)) + const edges = [[0, 1],[0, 3],[0, 2],[1, 2],[3, 2],[3, 1]] + for (let [s, d] of edges) { + __graph__.addEdge(s, d) + } + const TEST_1 = JSON.stringify(__graph__.getConnections(0)) === '[1,3,2]' + const TEST_2 = JSON.stringify(__graph__.getConnections(2)) === '[0,1,3]' + const TEST_3 = JSON.stringify(__graph__.getConnections(3)) === '[0,2,1]' + const TEST_4 = JSON.stringify(__graph__.getConnections(4)) === '[]' + const TEST_5 = __graph__.getConnections(5) === null + return TEST_1 && TEST_2 && TEST_3 && TEST_4 && TEST_5 + })()`, + message: `The getConnections method returns the adjacency list for the given vertex or null if the vertex doesn't exist: @param {(string|number)} vertex` + }, + { + expression: `(() => { + if (isTestDisabled(Graph, 'isEmpty')) return 'DISABLED' + const TEST_1 = __graph__.isEmpty() === false + __graph__.__clearGraph__() + const TEST_2 = __graph__.isEmpty() === true + return TEST_1 && TEST_2 + })()`, + message: `The Graph class has an isEmpty method which returns true if the graph is empty, false if not` + }, + { + method: 'deepEqual', + expression: `(() => { + if (isTestDisabled(Graph, 'clear')) return 'DISABLED' + __graph__.clear() + return __graph__.numEdges === 0 && __graph__.__entries__() + })()`, + expected: [], + message: `The Graph class has a clear method which clears the graph's internal Map object and resets the numEdges propery to 0` + }, + { + expression: `(() => { + if (isTestDisabled(Graph, 'hasVertex')) return 'DISABLED' + const TEST_1 = __graph__.hasVertex('A') === true + const TEST_2 = __graph__.hasVertex('J') === false + return TEST_1 && TEST_2 + })()`, + message: `The Graph class has a hasVertex method which returns true if the graph has the given vertex, false if not: @param {(string|number)} vertex` + }, + { + expression: `(() => { + if (isTestDisabled(Graph, 'hasVertices')) return 'DISABLED' + const TEST_1 = __graph__.hasVertices('A', 'B') === true + const TEST_2 = __graph__.hasVertices('A', 'J') === false + const TEST_3 = __graph__.hasVertices('J', 'A') === false + const TEST_4 = __graph__.hasVertices('J', 'J') === false + return TEST_1 && TEST_2 && TEST_3 && TEST_4 + })()`, + message: `The Graph class has a hasVertices method which returns true if the graph has both given vertices, false if not: @param {(string|number)} vertexOne @param {(string|number)} vertexTwo` + } +] diff --git a/src/assets/tests/data-structures/HashTable.js b/src/assets/tests/data-structures/HashTable.js new file mode 100644 index 0000000..ffc7e76 --- /dev/null +++ b/src/assets/tests/data-structures/HashTable.js @@ -0,0 +1,167 @@ +export const tail = ` +if (typeof new HashTable() === 'object') { + HashTable.prototype.__clearTable__ = function() { + this.collection = {} + return true + } + HashTable.prototype.__print__ = function() { + return JSON.stringify(this.collection) + } +} + +let __table__ +const testHooks = { + beforeAll: () => { + __table__ = new HashTable() + }, + beforeEach: () => { + __table__.__clearTable__() + }, + afterAll: () => { + __table__ = null + } +} + +const isProperlyHashed = (tests, index) => { + if (!__table__.collection[1363]) { + tests[index].message = 'There is no value stored at the expected hash key. Be sure to hash your key when you add using hash method and store your key/value pair at the key it returns.' + return false + } + return true +} +` + +export const tests = [ + { + expression: `typeof __table__ === 'object'`, + message: `The HashTable data structure exists` + }, + { + expression: `typeof __table__.collection === 'object' && JSON.stringify(__table__.collection) === '{}'`, + message: `The HashTable data structure has a property called collection which intializes to an empty object literal` + }, + { + expression: `typeof __table__.hash === 'function'`, + message: `The HashTable class has a method called hash: @param {(string|number)} key @returns {string}` + }, + { + expression: `(() => { + const TEST_1 = __table__.hash('cool') === 429 + const TEST_2 = __table__.hash('whoah, I cant believe this actually works!') === 3900 + return TEST_1 && TEST_2 + })()`, + message: `For the tests to work properly, the hash method must return the sum of the given string's UTF-16 code units ( e.g. table.hash('cool') === 429 ). HINT: use String.charCodeAt(). NOTE: This is a naive hashing function, meant to demonstrate the concept of collision!` + }, + { + expression: `typeof __table__.add === 'function'`, + message: `The HashTable class has a method called add: @param {(string|number)} key @param {(string|number)} value(the key arg should be a string, and/or the hash method converts it to a string)` + }, + { + expression: `((tests) => { + __table__.add('Peter Weinberg', 7686) + if (!isProperlyHashed(tests, 5)) return false + const entry = JSON.stringify(__table__.collection[1363]) + return /Peter Weinberg/.test(entry) && /7686/.test(entry) + })(tests)`, + message: `The add method stores key/value pairs at hashed keys in the table's collection object` + }, + { + expression: `((tests) => { + __table__.add('Peter Weinberg', 7686) + if (!isProperlyHashed(tests, 6)) return false + return __table__.add('Peter Weinberg', 9000) === null + })(tests)`, + message: `The add method returns null when passed a key/value pair that shares the same key (before hashing) as a pair already stored in the table` + }, + { + expression: `typeof __table__.lookup === 'function'`, + message: `The HashTable class has a method called lookup, which takes an unhashed key as an argument: @param {(string|number)} key` + }, + { + expression: `((tests) => { + __table__.add('Peter Weinberg', 7686) + if (!isProperlyHashed(tests, 8)) return false + return __table__.lookup('Peter Weinberg') === 7686 + })(tests)`, + message: `The lookup method looks up a key by its hash, and returns the value pair associated with that key` + }, + { + expression: `(() => { + const TEST_1 = __table__.lookup('Peter Weinberg') === null + __table__.add('Peter Weinberg', 7686) + if (!isProperlyHashed(tests, 9)) return false + const TEST_2 = __table__.lookup('Cool') === null + return TEST_1 && TEST_2 + })()`, + message: `The lookup method returns null when called on an empty hash table or when no key/value pair is found at the given key` + }, + { + expression: `typeof __table__.remove === 'function'`, + message: `The HashTable class has a method called remove, which takes an unhashed key as an argument: @param {(string|number)} key` + }, + { + expression: `((tests) => { + __table__.add('Peter Weinberg', 7686) + if (!isProperlyHashed(tests, 11)) return false + return __table__.remove('Peter Weinberg') === 7686 && !__table__.collection[1363] + })(tests)`, + message: `The remove method removes key value pairs from table and returns the stored value` + }, + { + expression: `((tests) => { + if (!(__table__.remove('Cool!') === null)) return false + __table__.add('Peter Weinberg', 7686) + if (!(__table__.remove('Cool!') === null)) return false + return true + })(tests)`, + message: `The remove method returns null when called on an empty hash table or when no key/value pair is found at the given key` + }, + { + expression: `((tests) => { + __table__.add('Peter Weinberg', 7686) + __table__.add('Weinberg Peter', 8000) + + if (!isProperlyHashed(tests, 13)) return false + + if (__table__.collection[1363].length !== 2) { + tests[12].message = 'When 2 or more keys produce the same hash, key/value pair entries are stored in an array, or "bucket", at that hash.' + return false + } + + const TEST_1 = __table__.lookup('Weinberg Peter') === 8000 + const TEST_2 = __table__.lookup('Peter Weinberg') === 7686 + if (!TEST_1 || !TEST_2) { + tests[13].message = 'The lookup method correctly looks up two key/value pairs stored at the same hash key.' + return false + } + + const TEST_3 = __table__.remove('Peter Weinberg') === 7686 + if (!TEST_3) { + tests[13].message = 'The remove method correctly removes/returns values in instances of collision.' + return false + } + + if (!__table__.collection[1363]) { + tests[13].message = 'In instances of collision, be careful not to remove all values stored at a shared hash key.' + return false + } + + const TEST_4 = __table__.lookup('Peter Weinberg') === null + const TEST_5 = __table__.lookup('Weinberg Peter') === 8000 + if (!TEST_4 || !TEST_5) { + tests[13].message = 'When 2 key/value pairs are stored at the same hash key and one is removed, looking up the removed key returns null, looking up the other correctly returns the associated value.' + return false + } + + const TEST_6 = __table__.remove('Weinberg Peter') === 8000 + const TEST_7 = !__table__.collection[1363] + if (!TEST_6 || !TEST_7) { + tests[13].message = 'The remove method correctly removes/returns values in instances of collision.' + return false + } + + return true + })(tests)`, + message: `The hash table handles collisions (i.e. when more than one key/value pair produce the same hash key)` + } +] diff --git a/src/assets/tests/data-structures/LinkedList.js b/src/assets/tests/data-structures/LinkedList.js new file mode 100644 index 0000000..fb97eec --- /dev/null +++ b/src/assets/tests/data-structures/LinkedList.js @@ -0,0 +1,306 @@ +export const tail = ` +if (typeof new LinkedList() === 'object') { + LinkedList.prototype.__clearList__ = function() { + this.head = null + this.length = 0 + } +} +const checkNodes = (list) => { + if (typeof list.head.next === 'undefined' || + typeof list.head.value === 'undefined') { + console.log('WARNING: Nodes must have next and value properties for tests to work!') + return null + } +} + +let __list__ +const testHooks = { + beforeAll: () => { + __list__ = new LinkedList() + if (typeof __list__.head === 'undefined' || + typeof __list__.length === 'undefined' ) { + console.log( + 'WARNING: Linked List must have properties head and length for tests to work!\\n' + ) + } + }, + beforeEach: () => { + __list__.__clearList__() + }, + afterAll: () => { + __list__ = null + } +} +` +export const tests = [ + { + expression: `typeof __list__ === 'object'`, + message: 'The LinkedList data structure exists' + }, + { + expression: `__list__.head === null && __list__.length === 0`, + message: 'The LinkedList data structure has head and length properties, which initialize to null and 0, respectively' + }, + { + expression: `typeof __list__.add === 'function'`, + message: 'The LinkedList class has a method called add: @param {(number|string)} value' + }, + { + expression: ` + (() => { + __list__.add('cat') + checkNodes(__list__) + return __list__.head.value === 'cat' && __list__.head.next === null + })()`, + message: 'The add method assigns the first node added (with value and next properties) to the list\'s head' + }, + { + expression: ` + (() => { + __list__.add('cat') + __list__.add('dog') + const TEST_1 = __list__.head.next.value === 'dog' + __list__.add('bird') + __list__.add('pig') + const TEST_2 = __list__.head.next.next.next.value === 'pig' + const TEST_3 = __list__.head.next.next.next.next === null + return TEST_1 && TEST_2 + })()`, + message: 'Additional elements are appended to the tail node, such that each node keeps track of the next node. The last node has a next value of null' + }, + { + expression: ` + (() => { + __list__.add('cat') + __list__.add('dog') + const TEST_1 = __list__.length + __list__.add('bird') + const TEST_2 = __list__.add('pig') === true + const TEST_3 = __list__.length === 4 + return TEST_1 && TEST_2 && TEST_3 + })()`, + message: 'The add method returns a truthy value and increments the length property of the list by one for each node added to the list' + }, + { + expression: `typeof __list__.peekHead === 'function'`, + message: 'The LinkedList class has a method called peekHead' + }, + { + expression: ` + (() => { + __list__.add('cat') + __list__.add('dog') + return JSON.stringify(__list__.peekHead()) === '{"value":"cat","next":{"value":"dog","next":null}}' + })()`, + message: 'The peekHead method returns the head property of the LinkedList structure, so that you can easily and visually inspect the list' + }, + { + expression: `typeof __list__.remove === 'function'`, + message: 'The LinkedList class has a method called remove: @param {(number|string)} value' + }, + { + expression: ` + (() => { + __list__.add('cat') + __list__.add('dog') + __list__.add('bird') + __list__.remove('cat') + const TEST_1 = __list__.head.value === 'dog' && __list__.head.next.value === 'bird' + __list__.remove('dog') + const TEST_2 = __list__.head.value === 'bird' && __list__.head.next === null + __list__.remove('bird') + const TEST_3 = __list__.head === null + return TEST_1 && TEST_2 && TEST_3 + })()`, + message: 'When the first node is removed, the head node assumes the value of the removed node\'s next value' + }, + { + expression: ` + (() => { + __list__.add('cat') + __list__.add('dog') + __list__.remove('dog') + return __list__.head.next === null + })()`, + message: 'When the last, or tail node, of a list is removed, the previous node\'s next property is set to null' + }, + { + expression: ` + (() => { + __list__.add('cat') + __list__.add('dog') + __list__.add('bird') + __list__.remove('dog') + return __list__.head.next.value === 'bird' + })()`, + message: 'When a node that is neither the head or tail node is removed, the linked list structure and next references are correctly maintained' + }, + { + expression: ` + (() => { + __list__.add('cat') + __list__.add('bird') + __list__.add('pig') + __list__.add('cow') + const TEST_1 = __list__.remove('cat') && __list__.length === 3 + const TEST_2 = __list__.remove('pig') && __list__.length === 2 + const TEST_3 = __list__.remove('cow') && __list__.length === 1 + const TEST_4 = __list__.remove('bird') && __list__.length === 0 + return TEST_1 && TEST_2 && TEST_3 && TEST_4 + })()`, + message: 'For every node removed from the list, the remove method returns a truthy value and decrements the length of the list by one' + }, + { + expression: ` + (() => { + const TEST_1 = __list__.remove('cat') === null + __list__.add('dog') + __list__.add('cat') + const TEST_2 = __list__.remove('bird') === null + return TEST_1 && TEST_2 && __list__.length === 2 + })()`, + message: 'If remove is called on an empty list, or finds no matching value to remove, null is returned and the list\'s length property is un-mutated' + }, + { + expression: `typeof __list__.indexOf === 'function'`, + message: 'The LinkedList class has a method called indexOf: @param {(number|string)} value' + }, + { + expression: ` + (() => { + const TEST_1 = __list__.indexOf('cat') === -1 + __list__.add('cat') + __list__.add('dog') + __list__.add('bird') + const TEST_2 = __list__.indexOf('bird') === 2 + __list__.add('pig') + __list__.add('cow') + const TEST_3 = __list__.indexOf('cow') === 4 + __list__.remove('dog') + const TEST_4 = __list__.indexOf('bird') === 1 + const TEST_5 = __list__.indexOf('monkey') === -1 + return TEST_1 && TEST_2 && TEST_3 && TEST_4 && TEST_5 + })()`, + message: 'The indexOf method returns the zero-based index of the given element or -1 if it doesn\'t exist: @param {(number|string)} value' + }, + { + expression: `typeof __list__.elementAt === 'function'`, + message: 'The LinkedList class has a method called elementAt: @param {number} index' + }, + { + expression: ` + (() => { + const TEST_1 = __list__.elementAt(0) === null + __list__.add('cat') + const TEST_2 = __list__.elementAt(1) === null + __list__.add('dog') + const TEST_3 = __list__.elementAt(1) === 'dog' + const TEST_4 = __list__.elementAt(0) === 'cat' + __list__.add('pig') + __list__.add('bird') + __list__.add('toad') + const TEST_5 = __list__.elementAt(3) === 'bird' + __list__.remove('bird') + const TEST_6 = __list__.elementAt(3) === 'toad' + const TEST_7 = __list__.elementAt(5) === null + const TEST_8 = __list__.elementAt(-5) === null + return TEST_1 && TEST_2 && TEST_3 && TEST_4 && TEST_5 && TEST_6 && TEST_7 && TEST_8 + })()`, + message: 'The elementAt method returns the value at the given index, or null if the given index is out of scope' + }, + { + expression: `typeof __list__.removeAt === 'function'`, + message: 'The LinkedList class has a method called removeAt: @param {number} index' + }, + { + expression: ` + (() => { + __list__.add('cat') + __list__.add('dog') + __list__.add('bird') + __list__.add('fish') + const TEST_1 = __list__.removeAt(1) === 'dog' && __list__.head.next.value === 'bird' + const TEST_2 = __list__.removeAt(0) === 'cat' && __list__.head.value === 'bird' && __list__.head.next.value === 'fish' + const TEST_3 = __list__.removeAt(1) === 'fish' && __list__.head.next === null + const TEST_4 = __list__.removeAt(0) === 'bird' && __list__.head === null + return TEST_1 && TEST_2 && TEST_3 && TEST_4 + })()`, + message: 'The removeAt method removes the node at the given index and returns its value, while retaining the linked list structure/references (consider each of the cases outlined in the __list__.remove(\'val\') tests above)' + }, + { + expression: ` + (() => { + __list__.add('cat') + __list__.add('dog') + __list__.add('kitten') + const TEST_1 = __list__.length === 3 + __list__.removeAt(1) + const TEST_2 = __list__.length === 2 + __list__.removeAt(1) + const TEST_3 = __list__.length === 1 + __list__.removeAt(1) // no change + __list__.removeAt(0) + const TEST_4 = __list__.length === 0 + return TEST_1 && TEST_2 && TEST_3 && TEST_4 + })()`, + message: 'The removeAt method decrements the length of the list by one for every node removed from the list' + }, + { + expression: ` + (() => { + const TEST_1 = __list__.removeAt(0) === null + __list__.add('cat') + const TEST_2 = __list__.removeAt(1) === null + const TEST_3 = __list__.removeAt(5) === null + const TEST_4 = __list__.removeAt(-5) === null + return TEST_1 && TEST_2 && TEST_3 && TEST_4 + })()`, + message: 'The removeAt method returns null if the given index is less than 0, greater than or equal to the length of the list, or if the list is empty' + }, + { + expression: `typeof __list__.addAt === 'function'`, + message: 'The LinkedList class has a method called addAt: @param {number} index @param {(string|number)} value' + }, + { + expression: ` + (() => { + __list__.add('cat') + __list__.add('dog') + __list__.addAt(1, 'bird') + return __list__.head.value === 'cat' && __list__.head.next.value === 'bird' && __list__.head.next.next.value === 'dog' + })()`, + message: 'The addAt method adds the given value to the list at the given index, while maintaining the linked-list structure/references' + }, + { + expression: ` + (() => { + __list__.add('cat') + __list__.addAt(0, 'bird') + return __list__.head.value === 'bird' && __list__.head.next.value === 'cat' && __list__.head.next.next === null + })()`, + message: 'When the given index is 0, the value passed to addAt becomes the new head node, referencing the rest of the list in its next property' + }, + { + expression: ` + (() => { + const TEST_1 = __list__.addAt(0, 'cat') === null + __list__.add('cat') + __list__.add('dog') + const TEST_2 = __list__.addAt(4, 'cat') === null + const TEST_3 = __list__.addAt(-4, 'cat') === null + return TEST_1 && TEST_2 && TEST_3 + })()`, + message: 'The addAt method returns null if the given index is less than 0, greater than or equal to the length of the list, or if the list is empty' + }, + { + expression: ` + (() => { + __list__.add('cat') + __list__.add('dog') + __list__.addAt(0, 'bird') + const TEST_1 = __list__.addAt(1, 'fish') === true + return TEST_1 && __list__.length === 4 + })()`, + message: 'The addAt method returns a truthy value and increments the length of the linked list by one for each node added to the list' + } +] diff --git a/src/assets/tests/data-structures/MaxHeap.js b/src/assets/tests/data-structures/MaxHeap.js new file mode 100644 index 0000000..4796efa --- /dev/null +++ b/src/assets/tests/data-structures/MaxHeap.js @@ -0,0 +1,109 @@ +export const tail = ` +if (typeof new MaxHeap() === 'object') { + MaxHeap.prototype.__clear__ = function() { + this.heap = [] + return true + } +} + +let __heap__ +const testHooks = { + beforeAll: () => { + __heap__ = new MaxHeap() + }, + beforeEach: () => { + __heap__.__clear__() + typeof __heap__.insert === 'function' && + [7,10,14,32,2,64,37].forEach(n => __heap__.insert(n)) + }, + afterAll: () => { + __heap__ = null + } +} +` + +export const tests = [ + { + expression: `typeof __heap__ === 'object'`, + message: 'The MaxHeap data structure exists' + }, + { + expression: ` + (() => { + __heap__.__clear__() + return __heap__.heap && Array.isArray(__heap__.heap) && __heap__.heap.length === 0 + })()`, + message: 'The MaxHeap data structure has a heap property, initialized as an empty Array object' + }, + { + expression: `typeof __heap__.insert == 'function'`, + message: 'MaxHeap has a method called insert: @param {number} number' + }, + { + expression: `JSON.stringify(__heap__.heap) === '[64,14,37,7,2,10,32]'`, + message: 'The insert method adds elements according to the max heap property' + }, + { + expression: `typeof __heap__.remove == 'function'`, + message: 'MaxHeap has a method called remove' + }, + { + expression: ` + (() => { + if (__heap__.remove() !== 64) return false + if (JSON.stringify(__heap__.heap) !== '[37,14,32,7,2,10]') + return false + if (__heap__.remove() !== 37) return false + if (JSON.stringify(__heap__.heap) !== '[32,14,10,7,2]') + return false + if (__heap__.remove() !== 32) return false + if (JSON.stringify(__heap__.heap) !== '[14,2,10,7]') + return false + return true + })() + `, + message: 'The remove method removes and returns elements according to the max heap property' + }, + { + expression: `__heap__.__clear__() && __heap__.remove() === null`, + message: 'The remove method returns null when called on an empty heap' + }, + { + expression: `typeof __heap__.sort == 'function'`, + message: 'MaxHeap has a method called sort.' + }, + { + expression: `JSON.stringify(__heap__.sort()) === '[2,7,10,14,32,37,64]'`, + message: 'The sort method returns a sorted array (from least to greatest) containing all the elements in the heap' + }, + { + expression: `typeof __heap__.size == 'function' || typeof __heap__.size == 'number'`, + message: 'MaxHeap has a method or property called size' + }, + { + expression: ` + (() => { + if (typeof __heap__.size === 'undefined') + return false + if (typeof __heap__.size === 'function') { + if (__heap__.size() !== 7) return false + __heap__.insert(64) + __heap__.insert(37) + if (__heap__.size() !== 9) return false + __heap__.remove() + __heap__.remove() + if (__heap__.size() !== 7) return false + } else if (typeof __heap__.size === 'number') { + if (__heap__.size !== 7) return false + __heap__.insert(64) + __heap__.insert(37) + if (__heap__.size !== 9) return false + __heap__.remove() + __heap__.remove() + if (__heap__.size !== 7) return false + } + return true + })()`, + message: 'The size method returns the correct size of the heap' + } +] diff --git a/src/assets/tests/data-structures/PriorityQueue.js b/src/assets/tests/data-structures/PriorityQueue.js new file mode 100644 index 0000000..f1352b9 --- /dev/null +++ b/src/assets/tests/data-structures/PriorityQueue.js @@ -0,0 +1,268 @@ +export const tail = ` +if (typeof new PriorityQueue() === 'object') { + PriorityQueue.prototype.__print__ = function() { + if (!this.head) { + return null + } + let result = [] + let node = this.head + while(node) { + result.push(node.value) + node = node.next + } + return result.join('') + } + PriorityQueue.prototype.__dequeue__ = function() { + if (!this.head) { + return null + } + const value = this.head.value + this.head = this.head.next + this.size-- + return value + } + PriorityQueue.prototype.__clear__ = function() { + this.head = null + this.tail = null + this.size = 0 + this.length = 0 + } +} + +const checkNodes = (pq) => { + if (typeof pq.head.value === 'undefined' || + typeof pq.head.next === 'undefined' || + typeof pq.head.priority === 'undefined' ) { + console.log('WARNING: Nodes must have value, next and priority properties for tests to work!') + } +} + +let __pq__ +const testHooks = { + beforeAll: () => { + __pq__ = new PriorityQueue() + if (typeof __pq__.head === 'undefined' || + typeof __pq__.size === 'undefined' ) { + console.log( + 'WARNING: Priority Queue must have properties head and size for tests to work!\\n' + ) + } + }, + beforeEach: () => { + __pq__.__clear__() + }, + afterAll: () => { + __pq__ = null + } +} +` + +export const tests = [ + { + expression: `typeof __pq__ === 'object'`, + message: `The PriorityQueue data structure exists` + }, + { + expression: ` + (() => { + return __pq__.head === null && __pq__.size === 0 + })()`, + message: `The PriorityQueue data structure has a head and size properties which initialize to null and 0 respectively` + }, + { + expression: `typeof __pq__.enqueue === 'function'`, + message: `The PriorityQueue has an enqueue method: @param {(number|string)} value @param {number} priority` + }, + { + expression: ` + (() => { + const pairs = [[3, 3],[0, 0],[50, 50],[4, 4],[10, 10],[5, 5],[2, 2]] + for (let [_0_, _1_] of pairs) { + __pq__.enqueue(_0_, _1_) + } + checkNodes(__pq__) + return __pq__.__print__() === '023451050' + })()`, + message: `The enqueue method inserts values into the queue according to priority (lowest priority at the head, greatest priority at the tail)` + }, + { + expression: ` + (() => { + const pairs = [[3, 3],['two', 2],[0, 0],['two-a', 2],[5, 5],['two-b', 2]] + for (let [_0_, _1_] of pairs) { + __pq__.enqueue(_0_, _1_) + } + return __pq__.__print__() === '0twotwo-atwo-b35' + })()`, + message: `When two or more elements have the same priority, the enqueue method treats the elements inserted first as having higher precedence (will be dequeued first)` + }, + { + expression: ` + (() => { + const pairs = [[null, null],[null, '50'],[null, {}],[null, []]] + for (let [_0_, _1_] of pairs) { + if (__pq__.enqueue(_0_, _1_) !== null) + return false + } + return true + })()`, + message: `The enqueue method returns null if the second argument is anything except a number` + }, + { + expression: ` + (() => { + __pq__.enqueue(3, 3) + __pq__.enqueue(0, 0) + if (__pq__.size !== 2) return false + __pq__.enqueue(50, 50) + __pq__.enqueue(50, '50') + __pq__.enqueue(50, null) + __pq__.enqueue(4, 4) + if (__pq__.size !== 4) return false + return true + })()`, + message: `The enqueue method increments the size property by 1 each time an element is successfully added to the queue` + }, + { + expression: `typeof __pq__.dequeue === 'function'`, + message: `The PriorityQueue has a method called dequeue` + }, + { + expression: ` + (() => { + const pairs = [[3, 3],[0, 0],[5, 5],[4, 4],[1, 1],[2, 2]] + for (let [_0_, _1_] of pairs) { + __pq__.enqueue(_0_, _1_) + } + let result = '' + let i = 5 + while (i > 0) { + result += __pq__.dequeue() + i-- + } + return result === '01234' && __pq__.__print__() === '5' + })()`, + message: `The dequeue method removes and returns elements according to their priority (lower priorites take precedence, and are dequeued first)` + }, + { + expression: ` + (() => { + __pq__.enqueue(3, 3) + __pq__.enqueue(0, 0) + __pq__.enqueue(5, 5) + let i = 3 + while (i > 0) { + __pq__.dequeue() + i-- + } + return __pq__.head === null + })()`, + message: `The dequeue method sets the head property to null when the last element is dequeued` + }, + { + expression: ` + (() => { + __pq__.enqueue(3, 3) + __pq__.enqueue(0, 0) + __pq__.enqueue(5, 5) + let i = 3 + while (i > 0) { + __pq__.dequeue() + if (__pq__.size !== i-1) return false + i-- + } + return true + })()`, + message: `The dequeue decrements the size property by 1 for every element removed from the queue` + }, + { + expression: `typeof __pq__.front === 'function'`, + message: `The PriorityQueue has a method called front` + }, + { + expression: ` + (() => { + __pq__.enqueue(3, 3) + __pq__.enqueue(0, 0) + __pq__.enqueue(5, 5) + const TEST_1 = __pq__.front() === 0 + const TEST_2 = __pq__.__print__().length === 3 + __pq__.__dequeue__() + const TEST_3 = __pq__.front() === 3 + const TEST_4 = __pq__.__print__().length === 2 + return TEST_1 && TEST_2 && TEST_3 && TEST_4 + })()`, + message: `The front method returns the element at the front, or top, of the queue, without removing it` + }, + { + expression: `typeof __pq__.isEmpty === 'function'`, + message: `The PriorityQueue has a method called isEmpty` + }, + { + expression: ` + (() => { + const TEST_1 = __pq__.isEmpty() === true + __pq__.enqueue(3, 3) + const TEST_2 = __pq__.isEmpty() === false + return TEST_1 && TEST_2 + })()`, + message: `The isEmpty method returns true is the queue is empty, and false if not` + }, + { + expression: ` + (() => { + if (isTestDisabled(PriorityQueue, 'contains')) { + return 'DISABLED' + } + if (__pq__.contains('a')) return false + __pq__.enqueue(0, 0) + __pq__.enqueue('0', 6) + __pq__.enqueue(2, 2) + return __pq__.contains(2) && __pq__.contains('0') && !__pq__.contains(9) + })()`, + message: `The contains method returns true if an element is present in the queue and false if not: @param {(number|string)} value` + }, + { + expression: ` + (() => { + if (isTestDisabled(PriorityQueue, 'priorityOf')) { + return 'DISABLED' + } + const TEST_1 = __pq__.priorityOf(3) === null + __pq__.enqueue(3, 3) + __pq__.enqueue(0, 0) + const TEST_2 = __pq__.priorityOf(4) === null + __pq__.enqueue('5', 5) + return TEST_1 && TEST_2 && __pq__.priorityOf(3) === 3 && __pq__.priorityOf('5') === 5 + })()`, + message: `The priorityOf method returns the priority of a given element or null if the given element doesn't exist: @param {(number|string)} value` + }, + { + expression: ` + (() => { + if (isTestDisabled(PriorityQueue, 'elementAt')) { + return 'DISABLED' + } + if (__pq__.elementAt(2) !== null) return false + __pq__.enqueue(3, 3) + if (__pq__.elementAt(2) !== null) return false + __pq__.enqueue(0, 0) + __pq__.enqueue('5', 5) + if (__pq__.elementAt(3) !== 3) return false + if (__pq__.elementAt(5) !== '5') return false + return true + })()`, + message: `The elementAt method returns the element at the given priority or null if no such element exists: @param {number} priority` + }, + { + expression: ` + (() => { + if (isTestDisabled(PriorityQueue, 'elementAt')) { + return 'DISABLED' + } + __pq__.enqueue(3, 3) + return __pq__.elementAt('3') === null && __pq__.elementAt(true) === null + })()`, + message: `The elementAt method returns null if passed an argument that has a type other than 'number'` + }, +] diff --git a/src/assets/tests/data-structures/Queue.js b/src/assets/tests/data-structures/Queue.js new file mode 100644 index 0000000..acabc03 --- /dev/null +++ b/src/assets/tests/data-structures/Queue.js @@ -0,0 +1,140 @@ +export const tail = ` +if (typeof new Queue() === 'object') { + Queue.prototype.__print__ = function() { + if (!this.root) { + return null + } + + let result = [] + let node = this.root + + while(node) { + result.push(node.value) + node = node.next + } + + return result.join('') + } + Queue.prototype.__clear__ = function() { + this.root = null + this.tail = null + this.length = 0 + } +} + +const checkNodes = (q) => { + if (typeof q.root.value === 'undefined' || + typeof q.root.next === 'undefined') { + console.log('WARNING: Nodes must have value and next properties for __queue__s to work!') + } +} + +let __queue__ +const testHooks = { + beforeAll: () => { + __queue__ = new Queue() + }, + beforeEach: () => { + __queue__.__clear__() + }, + afterAll: () => { + __queue__ = null + } +} +` + +export const tests = [ + { + expression: `typeof __queue__ === 'object'`, + message: `The Queue data structure exists.` + }, + { + expression: `__queue__.root === null`, + message: 'The queue has a root property which initializes to null' + }, + { + expression: `typeof __queue__.enqueue === 'function'`, + message: 'The Queue class has an enqueue method: @param {(number|string)} value' + }, + { + expression: ` + (() => { + __queue__.enqueue('one') + __queue__.enqueue('two') + __queue__.enqueue('three') + checkNodes(__queue__) + const qstring = __queue__.__print__() + return /one/.test(qstring) && + /two/.test(qstring) && + /three/.test(qstring) && + qstring.length === 11 + })()`, + message: 'The enqueue method adds elements to the queue' + }, + { + expression: `typeof __queue__.dequeue === 'function'`, + message: 'The Queue class has a dequeue method' + }, + { + expression: `(() => { + __queue__.enqueue('one') + __queue__.enqueue('two') + __queue__.enqueue('three') + const TEST_1 = __queue__.dequeue() === 'one' + const TEST_2 = __queue__.dequeue() === 'two' + return TEST_1 && TEST_2 && __queue__.__print__() === 'three' + })()`, + message: 'The dequeue method removes and returns the elements from the queue according to the first-in-first-out principle' + }, + { + expression: `typeof __queue__.front === 'function'`, + message: 'The Queue class has a front method' + }, + { + expression: ` + (() => { + __queue__.enqueue('one') + __queue__.enqueue('two') + const front = __queue__.front() === 'one' + const qstring = __queue__.__print__() + return /one/.test(qstring) && + /two/.test(qstring) && + front + })()`, + message: 'The front method returns value of the front element of the queue, without removing it' + }, + { + expression: `typeof __queue__.size === 'function' || typeof __queue__.size === 'number'`, + message: 'The Queue class has a size method' + }, + { + expression: `(() => { + const TEST_1 = typeof __queue__.size === 'function' + ? __queue__.size() === 0 + : __queue__.size === 0 + __queue__.enqueue('one') + __queue__.enqueue('two') + const TEST_2 = typeof __queue__.size === 'function' + ? __queue__.size() === 2 + : __queue__.size === 2 + __queue__.dequeue() + const three = typeof __queue__.size === 'function' + ? __queue__.size() === 1 + : __queue__.size === 1 + return TEST_1 && TEST_2 && three + })()`, + message: 'The size method returns the correct length of the queue' + }, + { + expression: `typeof __queue__.isEmpty === 'function'`, + message: 'The Queue class has an isEmpty method' + }, + { + expression: `(() => { + const empty = __queue__.isEmpty() === true + __queue__.enqueue('one') + return !__queue__.isEmpty() && empty + })()`, + message: 'The isEmpty method returns true if the queue is empty, and false if not' + }, +] diff --git a/src/assets/tests/data-structures/Stack.js b/src/assets/tests/data-structures/Stack.js new file mode 100644 index 0000000..9f7b459 --- /dev/null +++ b/src/assets/tests/data-structures/Stack.js @@ -0,0 +1,157 @@ +// LIFO +export const tail = ` +if (typeof new Stack() === 'object') { + Stack.prototype.__print__ = function() { + if (!this.root) { + return '[]' + } + const result = [] + let node = this.root + while (node.next) { + result.push(node.value) + node = node.next + } + result.push(node.value) + return result.join('') + } + Stack.prototype.__pop__ = function() { + if (!this.root) { + return null + } + const value = this.root.value + this.root = this.root.next + return value + } + Stack.prototype.__clear__ = function() { + this.root = null + this.size = 0 + } +} + +let __stack__ +const testHooks = { + beforeAll: () => { + __stack__ = new Stack() + }, + beforeEach: () => { + __stack__.__clear__() + }, + afterAll: () => { + __stack__ = null + } +} +` +export const tests = [ + { + expression: `typeof __stack__ === 'object'`, + message: 'The Stack data structure exists' + }, + { + expression: `__stack__.root === null`, + message: 'The Stack data structure has a propery called root which initializes to null' + }, + { + expression: `typeof __stack__.push === 'function'`, + message: 'The stack has a method called push: @param {(number|string)} value' + }, + { + expression: ` + (() => { + __stack__.push(5) + const TEST_1 = __stack__.root.value === 5 + __stack__.push(4) + const TEST_2 = __stack__.root.value === 4 && __stack__.root.next.value === 5 && __stack__.root.next.next === null + return TEST_1 + })()`, + message: `The push creates a new Node with properties value and next, where value is the pushed element and next is null or the next element in the stack` + }, + { + expression: ` + (() => { + [5,4,3,2,1].forEach(n => __stack__.push(n)) + return __stack__.__print__() === '12345' + })() + `, + message: 'The push method adds elements to the top of the stack, according to the first-in-first-out principle' + }, + { + expression: `typeof __stack__.pop === 'function'`, + message: 'The stack has a method called pop' + }, + { + expression: ` + (() => { + [5,4,3,2,1].forEach(n => __stack__.push(n)) + const beforePop = __stack__.__print__() === '12345' + const pop_1 = __stack__.pop() + const pop_2 = __stack__.pop() + const pop_3 = __stack__.pop() + const afterPop = __stack__.__print__() === '45' + return beforePop && pop_1 === 1 && pop_2 === 2 && pop_3 === 3 && afterPop + })() + `, + message: 'The pop method removes and returns elements from top of the stack, according to the first-in-first-out principle' + }, + { + expression: `__stack__.pop() === null`, + message: 'The pop method returns null when called on an empty stack' + }, + { + expression: `typeof __stack__.peek === 'function'`, + message: 'The stack has a method called peek' + }, + { + expression: ` + (() => { + [5,4,3,2,1].forEach(n => __stack__.push(n)); + const peek_1 = __stack__.peek() + const afterPeek_1 = __stack__.__print__() === '12345' + __stack__.__pop__() + __stack__.__pop__() + const peek_2 = __stack__.peek() + const afterPeek_2 = __stack__.__print__() === '345' + __stack__.push(500) + const peek_3 = __stack__.peek() + return peek_1 === 1 && afterPeek_1 && peek_2 === 3 && afterPeek_2 && peek_3 === 500 + })() + `, + message: 'The peek method returns elements from top of the stack, without modifying the stack' + }, + { + expression: `__stack__.peek() === null`, + message: 'The peek method returns null when called on an empty stack' + }, + { + expression: `typeof __stack__.isEmpty === 'function'`, + message: 'The stack has a method called isEmpty' + }, + { + expression: ` + (() => { + const TEST_1 = __stack__.isEmpty() + __stack__.push(5) + const TEST_2 = __stack__.isEmpty() + ;[4,3,2,1].forEach(n => __stack__.push(n)) + const TEST_3 = __stack__.isEmpty() + return TEST_1 && !TEST_2 && !TEST_3 + })() + `, + message: 'The isEmpty method returns true for an empty stack, and false otherwise' + }, + { + expression: `typeof __stack__.clear === 'function'`, + message: 'The stack has a method called clear' + }, + { + expression: ` + (() => { + [5,4,3,2,1].forEach(n => __stack__.push(n)) + const before = __stack__.__print__() === '12345' + __stack__.clear() + const after = __stack__.root === null + return before && after + })() + `, + message: 'The clear method clears the stack, and resets the stack\'s root to null' + }, +] diff --git a/src/components/CodeMirrorRenderer.js b/src/components/CodeMirrorRenderer.js index 377f86d..ddd2d05 100644 --- a/src/components/CodeMirrorRenderer.js +++ b/src/components/CodeMirrorRenderer.js @@ -1,65 +1,74 @@ -import { connect } from 'react-redux'; -import { Controlled as CodeMirror } from 'react-codemirror2'; -import defaultOptions from '../utils/editorConfig'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { updateCode } from '../actions/editor'; +import { connect } from 'react-redux' +import { Controlled as CodeMirror } from 'react-codemirror2' +import defaultOptions from '../utils/editorConfig' +import PropTypes from 'prop-types' +import React, { Component } from 'react' +import { updateCode } from '../actions/editor' // codemirror assets import '../styles/codemirror.css' -import 'codemirror/keymap/sublime'; -import 'codemirror/lib/codemirror.css'; -import 'codemirror/addon/fold/foldcode'; -import 'codemirror/addon/fold/foldgutter'; -import 'codemirror/addon/comment/comment'; -import 'codemirror/addon/fold/brace-fold'; -import 'codemirror/mode/markdown/markdown'; -import 'codemirror/addon/fold/comment-fold'; -import 'codemirror/addon/edit/matchbrackets'; -import 'codemirror/addon/edit/closebrackets'; -import 'codemirror/addon/fold/foldgutter.css'; -import 'codemirror/mode/javascript/javascript'; -import 'codemirror/addon/selection/active-line'; -import 'codemirror/theme/tomorrow-night-eighties.css'; +import 'codemirror/keymap/sublime' +import 'codemirror/lib/codemirror.css' +import 'codemirror/addon/fold/foldcode' +import 'codemirror/addon/hint/show-hint' +import 'codemirror/addon/fold/foldgutter' +import 'codemirror/addon/comment/comment' +import 'codemirror/addon/fold/brace-fold' +import 'codemirror/mode/markdown/markdown' +import 'codemirror/addon/hint/anyword-hint' +import 'codemirror/addon/fold/comment-fold' +import 'codemirror/addon/edit/matchbrackets' +import 'codemirror/addon/edit/closebrackets' +import 'codemirror/addon/hint/show-hint.css' +import 'codemirror/addon/fold/foldgutter.css' +import 'codemirror/mode/javascript/javascript' +import 'codemirror/addon/hint/javascript-hint' +import 'codemirror/addon/selection/active-line' +import 'codemirror/addon/search/match-highlighter' +import 'codemirror/theme/tomorrow-night-eighties.css' -import { JSHINT } from 'jshint'; -import 'codemirror/addon/lint/lint'; -import 'codemirror/addon/lint/lint.css'; -import 'codemirror/addon/lint/javascript-lint'; -window.JSHINT = JSHINT; +import { JSHINT } from 'jshint' +import 'codemirror/addon/lint/lint' +import 'codemirror/addon/lint/lint.css' +import 'codemirror/addon/lint/javascript-lint' +window.JSHINT = JSHINT class CodeMirrorRenderer extends Component { - componentDidUpdate(prevProps) { - if (prevProps.snippet !== this.props.snippet) { - this.props.updateCode(this.props.code, true); + componentDidMount() { + document.addEventListener('keydown', this.handleKeyPress) + } + componentWillUnmount() { + document.removeEventListener('keydown', this.handleKeyPress) + } + handleKeyPress = (e) => { + if ((e.ctrlKey || e.metaKey) && e.keyCode === 220) { + this.editor.focus() } } - handleFold = (editor, data) => { - // fold/hide BST solution's helper code - const { currentId, isSolution } = this.props; - const line_1 = (editor.getLine(1) === 'class QNode {'); - const line_9 = (editor.getLine(9) === 'class Queue {'); - if (currentId === 'BinarySearchTree' && isSolution && line_1 && line_9) { - editor.foldCode(1); - editor.foldCode(9); + componentDidUpdate(prevProps) { + if (prevProps.snippet !== this.props.snippet) { + this.props.updateCode(this.props.code, true) } } updateCode = (editor, data, value) => { - this.props.updateCode(value, false); + this.props.updateCode(value, false) + } + assignEditor = (editor) => { + this.editor = editor } render() { const options = this.props.welcome ? { ...defaultOptions, mode: 'markdown' - } : defaultOptions; + } : defaultOptions return ( - ); + ) } } @@ -80,4 +89,4 @@ const mapStateToProps = (state) => { } } -export default connect(mapStateToProps, { updateCode })(CodeMirrorRenderer); +export default connect(mapStateToProps, { updateCode })(CodeMirrorRenderer) diff --git a/src/components/Controls.js b/src/components/Controls.js index 6ac5257..72dc375 100644 --- a/src/components/Controls.js +++ b/src/components/Controls.js @@ -1,86 +1,111 @@ -import { clearConsole } from '../actions/console'; -import { connect } from 'react-redux'; -import React, { Component } from 'react'; -import '../styles/controls.css'; +import { clearConsole } from '../actions/console' +import { connect } from 'react-redux' +import executeCode from '../utils/test/challenge/eval-code-run-tests' +import React, { Component } from 'react' +import { RESET_STATE } from '../utils/regexp' +import PropTypes from 'prop-types' import { nextSnippet, previousSnippet, - resetEditorState -} from '../actions/editor'; + resetEditorState, + toggleSolution +} from '../actions/editor' + +import '../styles/controls.css' class Controls extends Component { constructor(props) { - super(props); + super(props) this.state = { clearConsole: false, resetCount: 0 } } componentDidMount() { - document.addEventListener('keydown', this.handleKeyPress); + document.addEventListener('keydown', this.handleKeyPress) } componentWillUnmount() { - document.removeEventListener('keydown', this.handleKeyPress); + document.removeEventListener('keydown', this.handleKeyPress) + } + componentDidUpdate(prevProps) { + if (prevProps.id !== this.props.id) { + this.setState({ clearConsole: true }) + } } handleKeyPress = (e) => { - if (e.ctrlKey && e.keyCode === 13) { - this.runCode(); + // Run Code: CMD/CTRL + ENTER + if ((e.ctrlKey || e.metaKey) && e.keyCode === 13) { + this.handleExecuteCode(this.props) } - if (e.ctrlKey && e.shiftKey && e.keyCode === 188) { - this.props.previousSnippet(); + // Next Snippet: CMD/CTRL + SHIFT + > + // Previous Snippet: CMD/CTRL + SHIFT + < + // Toggle Solution: CMD/CTRL + SHIFT + S + if ((e.ctrlKey || e.metaKey) && e.shiftKey) { + if (e.keyCode === 190) this.props.nextSnippet() + if (e.keyCode === 188) this.props.previousSnippet() + if (e.keyCode === 83) this.props.toggleSolution() } - if (e.ctrlKey && e.shiftKey && e.keyCode === 190) { - this.props.nextSnippet(); + // Clear Console: ALT + SHIFT + BACKSPACE + if (e.altKey && e.shiftKey && e.keyCode === 8) { + // prevent Backspace default + e.preventDefault() + this.props.clearConsole() } } toggleClearConsole = () => { if (this.state.clearConsole) { - this.props.clearConsole(); - this.setState({ clearConsole: false }); + this.props.clearConsole() + this.setState({ clearConsole: false }) } } + clearConsoleResetCount = () => { + this.setState({ resetCount: 0 }) + this.props.clearConsole() + } handleResetSate = () => { if (this.state.resetCount === 0) { - this.setState(state => ({ resetCount: state.resetCount+1 })); + this.setState(state => ({ resetCount: state.resetCount+1 })) + this.props.clearConsole() console.log( 'WARNING: Are you sure you want to reset?\n' + 'This action CANNOT be reversed, and all of\n' + 'your solutions will be permanently deleted.\n' + "To proceed, hit the 'Run Code' button again." - ); + ) + // reset count after 15 seconds to prevent accidental resets + setTimeout(() => { + if (this.state.resetCount === 1) { + this.clearConsoleResetCount() + console.log('Global state reset timed out. Please try again.') + } + }, 15000) } else { - this.props.resetEditorState(); - this.props.clearConsole(); - this.setState({ resetCount: 0 }); - console.log('State successfully reset!'); + this.props.resetEditorState() + this.clearConsoleResetCount() + console.log('State successfully reset!') } } - runCode = () => { - this.toggleClearConsole(); - if (/resetState\(\)/.test(this.props.code)) { - this.handleResetSate(); - } else if (/clearConsole\(\)/.test(this.props.code)) { - this.props.clearConsole(); + handleExecuteCode = ({ code, id }) => { + this.toggleClearConsole() + if (RESET_STATE.test(code)) { + this.handleResetSate() } else { - try { - // eslint-disable-next-line - eval(this.props.code); - } catch (error) { - console.log(error.toString()); - } - } - } - componentDidUpdate(prevProps) { - if (prevProps.slice !== this.props.slice) { - this.setState({ clearConsole: true }); + // if last action was handleResetSate and the resetState() call + // has been deleted reset resetCount to prevent accidental resets + this.state.resetCount === 1 && this.clearConsoleResetCount() + // run code && execute tests + executeCode( + code, + id + ) } } render() { return (
- ); + ) } -}; +} + +Controls.propTypes = { + clearConsole: PropTypes.func.isRequired, + code: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, + nextSnippet: PropTypes.func.isRequired, + previousSnippet: PropTypes.func.isRequired, + resetEditorState: PropTypes.func.isRequired, + toggleSolution: PropTypes.func.isRequired +} -const mapStateToProps = (state) => { +const mapStateToProps = ({ editor: { current } }) => { return { - code: state.editor.current.code, - slice: state.editor.current.code.slice(-20) + code: current.code, + id: current.id } } @@ -113,7 +148,8 @@ const mapDispatchToProps = { clearConsole, nextSnippet, previousSnippet, - resetEditorState + resetEditorState, + toggleSolution } -export default connect(mapStateToProps, mapDispatchToProps)(Controls); +export default connect(mapStateToProps, mapDispatchToProps)(Controls) diff --git a/src/components/sidebar/Console.js b/src/components/sidebar/Console.js new file mode 100644 index 0000000..82c9dd6 --- /dev/null +++ b/src/components/sidebar/Console.js @@ -0,0 +1,123 @@ +import { clearConsole } from '../../actions/console' +import { connect } from 'react-redux' +import { map } from 'lodash'; +import PropTypes from 'prop-types' +import React, { Component } from 'react' +import { ERROR_TYPES } from '../../utils/regexp' +import shortid from 'shortid' +import '../../styles/console.css' + +import { + ICON_BLACK, + ICON_DISABLED, + ICON_WHITE +} from '../../utils/base64' + +// button states: + +const disabled = { + background: '#1b1d1a', + color: '#787577', + icon: ICON_DISABLED +} + +const hover = { + background: '#ccc', + color: '#1b1d1a', + icon: ICON_BLACK +} + +const _default = { + background: '#1b1d1a', + color: '#ccc', + icon: ICON_WHITE +} + +class Console extends Component { + constructor(props) { + super(props) + this.state = disabled + } + componentWillReceiveProps(nextProps) { + if (!nextProps.messages.length) { + this.setState(disabled) + } else { + this.setState(_default) + } + } + handleMouseLeave = () => { + if (!this.props.messages.length) return + this.setState(_default) + } + handleMouseEnter = () => { + if (!this.props.messages.length) return + this.setState(hover) + } + renderMessages = (msg) => { + let className = ERROR_TYPES.test(msg) + ? 'sidebar--output--messages--message error' + : 'sidebar--output--messages--message' + return ( +

+ ) + } + render() { + const { background, color, icon } = this.state + const margin = { marginBottom: -4 } + + const buttonStyle = { + background, + color, + outline: `1px solid ${color}`, + userSelect: 'none' + } + + return ( +

+
+
+ Clear backspace icon +
+

+ {'// console output / tests:'} +

+ { map(this.props.messages, this.renderMessages) } +
+
+ ) + } +} + +Console.propTypes = { + bottomHeight: PropTypes.string.isRequired, + clearConsole: PropTypes.func.isRequired, + messages: PropTypes.array.isRequired, + transition: PropTypes.string.isRequired +} + +const mapStateToProps = ({ + console: messages, + panes: { + bottomHeight, + transition + } +}) => ({ + messages, + bottomHeight, + transition +}) + +export default connect(mapStateToProps, { clearConsole })(Console) diff --git a/src/components/sidebar/ConsoleOutput.js b/src/components/sidebar/ConsoleOutput.js deleted file mode 100644 index 8c663e5..0000000 --- a/src/components/sidebar/ConsoleOutput.js +++ /dev/null @@ -1,113 +0,0 @@ -import { clearConsole } from '../../actions/console'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import shortid from 'shortid'; -import '../../styles/console.css'; - -import { - ICON_BLACK, - ICON_DISABLED, - ICON_WHITE -} from '../../utils/base64'; - -// button states: - -const disabled = { - background: '#1b1d1a', - color: '#787577', - icon: ICON_DISABLED -}; - -const hover = { - background: '#ccc', - color: '#1b1d1a', - icon: ICON_BLACK -}; - -const _default = { - background: '#1b1d1a', - color: '#ccc', - icon: ICON_WHITE -}; - -const re = new RegExp('InternalError|RangeError|ReferenceError|EvalError|SyntaxError|TypeError|URIError'); - -class ConsoleOutput extends Component { - constructor(props) { - super(props); - this.state = disabled - } - componentWillReceiveProps(nextProps) { - if (!nextProps.messages.length) { - this.setState(disabled); - } else { - this.setState(_default); - } - } - handleMouseLeave = () => { - if (!this.props.messages.length) return; - this.setState(_default); - } - handleMouseEnter = () => { - if (!this.props.messages.length) return; - this.setState(hover); - } - renderMessages = (msg) => { - let className = 'sidebar--output--messages--message'; - if (re.test(msg)) className += ' error'; - return ( -

- {msg} -

- ); - } - render() { - - const { background, color, icon } = this.state; - const margin = { marginBottom: -4 }; - - const buttonStyle = { - background, - color, - outline: `1px solid ${color}` - }; - - return ( -
-
-
- Clear backspace icon -
-

- {'// console output:'} -

- { this.props.messages.map(this.renderMessages) } -
-
- ); - } -} - -ConsoleOutput.propTypes = { - attachRef: PropTypes.func.isRequired, - clearConsole: PropTypes.func.isRequired, - messages: PropTypes.array.isRequired -} - -const mapStateToProps = (state) => { - return { - messages: state.consoleOutput - } -} - -export default connect(mapStateToProps, { clearConsole })(ConsoleOutput); diff --git a/src/components/sidebar/Menu.js b/src/components/sidebar/Menu.js index ce27917..4fee0aa 100644 --- a/src/components/sidebar/Menu.js +++ b/src/components/sidebar/Menu.js @@ -1,19 +1,22 @@ -import CODE from '../../assets/codeRef'; -import MenuMap from './MenuMap'; -import PropTypes from 'prop-types'; -import React from 'react'; -import '../../styles/menu.css'; +import { CODE } from '../../assets/codeRef' +import { connect } from 'react-redux' +import MenuMap from './MenuMap' +import PropTypes from 'prop-types' +import React from 'react' +import '../../styles/menu.css' const { SORTING_ALGOS, DATA_STRUCTURES, EASY_ALGOS, MODERATE_ALGOS -} = CODE; +} = CODE -const Menu = ({ attachRef }) => { +const Menu = ({ topHeight, transition }) => { return ( -
+
Contents
@@ -37,11 +40,17 @@ const Menu = ({ attachRef }) => { xtraClass="sub" />
- ); + ) } Menu.propTypes = { - attachRef: PropTypes.func.isRequired, -}; + topHeight: PropTypes.string.isRequired, + transition: PropTypes.string.isRequired +} + +const mapStateToProps = ({ panes }) => ({ + topHeight: panes.topHeight, + transition: panes.transition +}) -export default Menu; +export default connect(mapStateToProps)(Menu) diff --git a/src/components/sidebar/MenuMap.js b/src/components/sidebar/MenuMap.js index c0a9f59..7c91b96 100644 --- a/src/components/sidebar/MenuMap.js +++ b/src/components/sidebar/MenuMap.js @@ -1,38 +1,43 @@ -import { closeModal, openModal } from '../../actions/modal'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { selectSnippet, selectSolution } from '../../actions/editor'; -import { selectTopic } from '../../actions/resources'; -import shortid from 'shortid'; +import { closeModal, openResourcesModal } from '../../actions/modal' +import { connect } from 'react-redux' +import PropTypes from 'prop-types' +import React, { Component } from 'react' +import { selectSnippet, selectSolution } from '../../actions/editor' +import shortid from 'shortid' +import _ from 'lodash' + +_.mixin({ + 'pascalCase': _.flow( + _.camelCase, + _.upperFirst + ) +}) class MenuMap extends Component { selectSeed = ({ currentTarget: { id }}) => { - this.props.selectSnippet(id); + this.props.selectSnippet(id) } selectSolution = (e) => { - e.stopPropagation(); - this.props.selectSolution(e.target.id); + e.stopPropagation() + this.props.selectSolution(e.target.id.slice(10)) } renderModal = (e) => { - e.stopPropagation(); - const modalId = e.target.id.slice(7); - this.props.selectTopic(modalId); - if (this.props.modalId === modalId && this.props.renderModal) { - this.props.closeModal(); - } else { - this.props.openModal(modalId); - } + e.stopPropagation() + const modalId = _.startCase(e.target.id.slice(7)) + this.props.modalId === modalId && this.props.renderModal + ? this.props.closeModal() + : this.props.openResourcesModal(modalId) } renderMenuItem = (item) => { - const bgColor = item.title.replace(/\s/g, '') === this.props.codeId + const id = _.pascalCase(item.title) + const background = id === this.props.codeId ? 'rgba(39, 145, 152, 0.52)' - : '#707070'; + : '#707070' return (
@@ -42,19 +47,19 @@ class MenuMap extends Component {
Solution Resources
}
- ); + ) } render() { return ( @@ -62,11 +67,11 @@ class MenuMap extends Component { {this.props.header} - {this.props.items.map(this.renderMenuItem)} + { _.map(this.props.items, this.renderMenuItem) } - ); + ) } -}; +} MenuMap.propTypes = { closeModal: PropTypes.func.isRequired, @@ -74,11 +79,10 @@ MenuMap.propTypes = { header: PropTypes.string.isRequired, items: PropTypes.array.isRequired, modalId: PropTypes.string.isRequired, - openModal: PropTypes.func.isRequired, + openResourcesModal: PropTypes.func.isRequired, renderModal: PropTypes.bool.isRequired, selectSnippet: PropTypes.func.isRequired, selectSolution: PropTypes.func.isRequired, - selectTopic: PropTypes.func.isRequired, xtraClass: PropTypes.string, } @@ -86,20 +90,17 @@ MenuMap.defaultProps = { xtraClass: '' } -const mapStateToProps = (state) => { - return { - modalId: state.modal.modalId, - renderModal: state.modal.renderModal, - codeId: state.editor.current.id - } -} +const mapStateToProps = (state) => ({ + modalId: state.modal.modalId, + renderModal: state.modal.renderModal, + codeId: state.editor.current.id +}) const mapDispatchToProps = { selectSnippet, selectSolution, - selectTopic, - openModal, + openResourcesModal, closeModal } -export default connect(mapStateToProps, mapDispatchToProps)(MenuMap); +export default connect(mapStateToProps, mapDispatchToProps)(MenuMap) diff --git a/src/components/utils/Divider.js b/src/components/utils/Divider.js index fbbf491..e5d1f85 100644 --- a/src/components/utils/Divider.js +++ b/src/components/utils/Divider.js @@ -1,18 +1,24 @@ -import PropTypes from 'prop-types'; -import React from 'react'; +import PropTypes from 'prop-types' +import React from 'react' +import { VERTICAL_GRIP, HORIZONTAL_GRIP } from '../../utils/base64' const Divider = ({ attachRef, direction }) => { return (
- ); -}; + ) +} Divider.propTypes = { direction: PropTypes.oneOf(['horizontal', 'vertical']), attachRef: PropTypes.func.isRequired -}; +} -export default Divider; +export default Divider diff --git a/src/components/utils/ErrorBoundary.js b/src/components/utils/ErrorBoundary.js index 74f5b3c..fc5f7cc 100644 --- a/src/components/utils/ErrorBoundary.js +++ b/src/components/utils/ErrorBoundary.js @@ -1,22 +1,22 @@ -import React, { Component } from 'react'; +import React, { Component } from 'react' class ErrorBoundary extends Component { constructor(props) { - super(props); + super(props) this.state = { hasError: false - }; + } } componentDidCatch(error, info) { console.log(error, info) - this.setState({ hasError: true }); + this.setState({ hasError: true }) } render() { if (this.state.hasError) { - return

Whoops! Our bad, it's probaby nothing. Try again!

; + return

Whoops! Our bad, it's probaby nothing. Try again!

} - return this.props.children; + return this.props.children } } -export default ErrorBoundary; +export default ErrorBoundary diff --git a/src/components/utils/Fader.js b/src/components/utils/Fader.js index 1638606..70d6ed5 100644 --- a/src/components/utils/Fader.js +++ b/src/components/utils/Fader.js @@ -1,20 +1,21 @@ -import React from 'react'; +import React from 'react' import { Transition } from 'react-transition-group' -const duration = 300; +const duration = 450 const defaultStyle = { - transition: `opacity ${duration}ms ease-in`, + transition: `opacity ${duration}ms ease-out`, opacity: 0 } const transitionStyles = { entering: { opacity: 0 }, entered: { opacity: 1 }, -}; + exiting: { opacity: 0 }, +} const Fade = ({ in: inProp, children, attachRef }) => ( - + {(state) => (
(
)}
-); +) -export default Fade; +export default Fade diff --git a/src/components/utils/Modal.js b/src/components/utils/Modal.js index 4ad2aa4..2b71e75 100644 --- a/src/components/utils/Modal.js +++ b/src/components/utils/Modal.js @@ -1,68 +1,100 @@ -import { connect } from 'react-redux'; -import { closeModal } from '../../actions/modal'; -import Fade from './Fader'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import ReactDOM from 'react-dom'; -import shortid from 'shortid'; -import '../../styles/modal.css'; +import { connect } from 'react-redux' +import { closeModal } from '../../actions/modal' +import Fade from './Fader' +import { map } from 'lodash'; +import PropTypes from 'prop-types' +import React, { Component } from 'react' +import ReactDOM from 'react-dom' +import shortid from 'shortid' +import '../../styles/modal.css' class Modal extends Component { componentDidMount() { - document.addEventListener('click', this.closeModal); + document.addEventListener('click', this.closeModal) } componentWillUnmount() { - document.removeEventListener('click', this.closeModal); + document.removeEventListener('click', this.closeModal) } closeModal = ({ target }) => { - if (!target.classList.contains('modal-trigger') && + if ( + this.props.renderModal && !this.modal.contains(target) && - this.props.renderModal) { - this.props.closeModal(); + !target.classList.contains('modal-trigger') + ) { + this.props.closeModal() } } - componentDidUpdate() { - if (this.props.renderModal) { - this.modal.parentNode.style.zIndex = '4'; - } else { - this.modal.parentNode.style.zIndex = '-4'; + renderListItem = (item) => { + if (this.props.modalType === 'resources') { + return ( +
  • + + {item.caption} + +
  • + ) } + // Announcement Modal: + return ( +
  • + ) + } + renderNumRemainingAnnouncements = () => { + let num + switch (localStorage.getItem('cs-pg-react-render-only-thrice')) { + case '1': num = 2; break + case '2': num = 1; break + default: num = 0 + } + return ( + + {`You will see this notification ${num} more time${num === 1 ? '' : 's'}`} + + ) } - renderListItem = (item) => ( -
  • - - {item.caption} - -
  • - ) render() { + const { renderModal, subHeader } = this.props return ReactDOM.createPortal( - this.modal = ref} in={this.props.renderModal}> + this.modal = ref} in={renderModal}>

    - { `${this.props.modalId.replace(/_/g, ' ')} Resources` } + { this.props.header }

    - { this.props.resources.map(this.renderListItem) } + { subHeader &&

    + +

    } +
      + { map(this.props.messages, this.renderListItem) } +
    + { this.props.modalType === 'announcement' && + this.renderNumRemainingAnnouncements() }
    , document.getElementById('modal-root') - ); + ) } } Modal.propTypes = { - resources: PropTypes.array.isRequired, + messages: PropTypes.array.isRequired, renderModal: PropTypes.bool.isRequired, - modalId: PropTypes.string.isRequired, + header: PropTypes.string.isRequired, closeModal: PropTypes.func.isRequired, + subHeader: PropTypes.string, + modalType: PropTypes.string.isRequired } -const mapStateToProps = (state) => { +const mapStateToProps = ({ modal }) => { return { - resources: state.resources, - renderModal: state.modal.renderModal, - modalId: state.modal.modalId + messages: modal.messages, + renderModal: modal.renderModal, + header: modal.modalId, + subHeader: modal.subHeader, + modalType: modal.modalType } } -export default connect(mapStateToProps, { closeModal })(Modal); +export default connect(mapStateToProps, { closeModal })(Modal) diff --git a/src/components/utils/Pane.js b/src/components/utils/Pane.js new file mode 100644 index 0000000..41f1c01 --- /dev/null +++ b/src/components/utils/Pane.js @@ -0,0 +1,33 @@ +import { connect } from 'react-redux' +import PropTypes from 'prop-types' +import React from 'react' + +const Pane = ({ + children, + className, + leftWidth, + rightWidth, + transition +}) => ( +
    + { children } +
    +) + +Pane.propTypes = { + children: PropTypes.array.isRequired, + className: PropTypes.string.isRequired, + leftWidth: PropTypes.string.isRequired, + rightWidth: PropTypes.string.isRequired +} + +const mapStateToProps = ({ panes }) => ({ + leftWidth: panes.leftWidth, + rightWidth: panes.rightWidth, +}) + +export default connect(mapStateToProps)(Pane) diff --git a/src/index.js b/src/index.js index 0569b6b..c8a99c7 100644 --- a/src/index.js +++ b/src/index.js @@ -1,38 +1,45 @@ -import App from './App'; -import { composeWithDevTools } from 'redux-devtools-extension'; -import { createStore } from 'redux'; -import ErrorBoundary from './components/utils/ErrorBoundary'; -import { hijackConsole } from './actions/console'; -import { Provider } from 'react-redux'; -import React from 'react'; -import ReactDOM from 'react-dom'; -import registerServiceWorker from './utils/registerServiceWorker'; -import rootReducer from './reducers/rootReducer'; -import simpleDrag from './utils/simpleDrag'; -import './styles/index.css'; +import App from './App' +import { composeWithDevTools } from 'redux-devtools-extension' +import { createStore } from 'redux' +import ErrorBoundary from './components/utils/ErrorBoundary' +import { hijackConsole } from './actions/console' +import { Provider } from 'react-redux' +import React from 'react' +import ReactDOM from 'react-dom' +import { DO_NOT_SAVE } from './utils/regexp' +import registerServiceWorker from './utils/registerServiceWorker' +import rootReducer from './reducers/rootReducer' +import simpleDrag from './utils/simpleDrag' +import './styles/index.css' // enable resizable split panes -simpleDrag(); +simpleDrag() // hijack console.log() function -hijackConsole(); +hijackConsole() export const store = createStore( rootReducer, composeWithDevTools() -); +) // set localStorage when navigating away from app window.onbeforeunload = function(e) { - const state = store.getState(); + const state = store.getState() // use // DO NOT SAVE comment to disable saving - if (!/\/\/\sDO\sNOT\sSAVE/i.test(state.editor.current.code)) { + if (!DO_NOT_SAVE.test(state.editor.current.code)) { localStorage.setItem( 'cs-pg-react-editorState', JSON.stringify(state.editor) - ); + ) + localStorage.setItem( + 'cs-pg-react-panesState', + JSON.stringify(state.panes) + ) } -}; + // save pane state + console.log('CS-Playground-React-State Saved!') +} ReactDOM.render( @@ -41,11 +48,11 @@ ReactDOM.render( , document.getElementById('root') -); +) if (module.hot) { module.hot.accept('./App', () => { - const NextApp = require('./App').default; + const NextApp = require('./App').default ReactDOM.render( @@ -53,8 +60,8 @@ if (module.hot) { , document.getElementById('root') - ); - }); + ) + }) } -registerServiceWorker(); +registerServiceWorker() diff --git a/src/reducers/console.js b/src/reducers/console.js index 5850363..819d5e6 100644 --- a/src/reducers/console.js +++ b/src/reducers/console.js @@ -1,15 +1,15 @@ -import { CLEAR_CONSOLE, CONSOLE_LOG } from '../actions/console'; +import * as types from '../actions/types' export default (state = [], action) => { switch(action.type) { - case CONSOLE_LOG: + case types.CONSOLE_LOG: return [ ...state, - action.messages - ]; - case CLEAR_CONSOLE: - return []; + action.logs + ] + case types.CLEAR_CONSOLE: + return [] default: - return state; + return state } } diff --git a/src/reducers/editor.js b/src/reducers/editor.js index 8233d4d..88e5e57 100644 --- a/src/reducers/editor.js +++ b/src/reducers/editor.js @@ -1,28 +1,14 @@ -import CODE from '../assets/codeRef'; -import WELCOME_MESSAGE from '../assets/seed/welcome'; +import * as types from '../actions/types' +import { CODE, SOLUTIONS } from '../assets/codeRef' +import { composeCodeStore, createOrderKey, populateCodeStore } from './utils' +import WELCOME_MESSAGE from '../assets/seed/welcome' +import { findIndex, indexOf, map } from 'lodash' -import { - NEXT_SNIPPET, - PREVIOUS_SNIPPET, - RESET_STATE, - SELECT_SNIPPET, - SELECT_SOLUTION, - UPDATE_CODE -} from '../actions/editor'; -// define reducer's initial state -const populateCodeStore = (arr) => { - for (let category in CODE) { - CODE[category].forEach(item => { - arr.push({ - id: item.title.replace(/\s/g, ''), - userCode: item.seed, - solutionCode: item.solution - }); - }); - } - return arr; -} +// NOTE: use to temporarily disable +// log action for reducer debugging +export const disableLogAction = false + const initialState = { welcome: true, @@ -31,52 +17,54 @@ const initialState = { code: WELCOME_MESSAGE, isSolution: false }, - codeStore: populateCodeStore([]) -}; + codeStore: populateCodeStore(CODE), + orderKey: createOrderKey(CODE) +} + // reducer's default state is either the initial state or // is pulled from local storage, which is set in index.js -// each time the user navigates away from the page. user -// may clear localStorage and reset this state by calling -// resetState() in the CodeMirror editor (not a true func) -// the user may also choose to NOT save their code to LS -// by leaving a // DO NOT SAVE single line comment in the -// editor before navigating away from the CSPG application let defaultState = JSON.parse( localStorage.getItem('cs-pg-react-editorState') -) || initialState; +) || initialState + + +// if lengths differ, call composeCodeStore to merge in +// new challenges and/or remove dupes from previous bug +if (initialState.codeStore.length !== defaultState.codeStore.length) { + const { codeStore, current } = composeCodeStore(initialState, defaultState) + defaultState.orderKey = createOrderKey(CODE) + defaultState.codeStore = codeStore + defaultState.current = current +} -// copy in any newly deployed changes to state saved in -// localStorage for users not accessing site over HTTPS -defaultState.codeStore = [ - ...defaultState.codeStore, - ...initialState.codeStore -]; // meaningless abstraction: -const updateCodeStore = (state) => { +const updateUserCode = (state) => { if (!state.current.isSolution && !state.welcome) { - return state.codeStore.map(codeObj => { + return map(state.codeStore, codeObj => { if (state.current.id === codeObj.id) { return { ...codeObj, userCode: state.current.code } } else { - return codeObj; + return codeObj } - }); + }) } else { - return state.codeStore; + return state.codeStore } } -export default (state = defaultState, action) => { + +const editor = (state = defaultState, action) => { switch(action.type) { - case RESET_STATE: - localStorage.removeItem('cs-pg-react-editorState'); - return initialState; - case UPDATE_CODE: + case types.RESET_STATE: + localStorage.removeItem('cs-pg-react-editorState') + localStorage.removeItem('cs-pg-react-suppress-tests-only-once') + return initialState + case types.UPDATE_CODE: return { ...state, current: { @@ -84,73 +72,70 @@ export default (state = defaultState, action) => { code: action.code } } - case SELECT_SOLUTION: - for (let codeObj of state.codeStore) { - if (codeObj.id === action.id) { - return { - welcome: false, - codeStore: updateCodeStore(state), - current: { - id: codeObj.id, - code: codeObj.solutionCode, - isSolution: true - } - } + case types.SELECT_SOLUTION: + return { + ...state, + welcome: false, + codeStore: updateUserCode(state), + current: { + id: action.id, + code: SOLUTIONS[action.id], + isSolution: true } } - break; - case SELECT_SNIPPET: - for (let codeObj of state.codeStore) { - if (codeObj.id === action.id) { - return { - welcome: false, - codeStore: updateCodeStore(state), - current: { - id: codeObj.id, - code: codeObj.userCode, - isSolution: false - } - } + case types.SELECT_SNIPPET: + let idx = findIndex(state.codeStore, { id: action.id }) + return { + ...state, + welcome: false, + codeStore: updateUserCode(state), + current: { + id: action.id, + code: state.codeStore[idx].userCode, + isSolution: false } } - break; - case NEXT_SNIPPET: - for (let i = 0; i < state.codeStore.length; i++) { - if (state.codeStore[i].id === state.current.id) { - let next; - if (state.welcome) next = 0; - else next = i === state.codeStore.length - 1 ? 0 : i+1; - return { - welcome: false, - codeStore: updateCodeStore(state), - current: { - id: state.codeStore[next].id, - code: state.codeStore[next].userCode, - isSolution: false - } - } + case types.TOGGLE_SOLUTION: + if (!SOLUTIONS[state.current.id]) + return state + return !state.current.isSolution + ? editor(state, { type: types.SELECT_SOLUTION, id: state.current.id }) + : editor(state, { type: types.SELECT_SNIPPET, id: state.current.id }) + case types.NEXT_SNIPPET: { + let { orderKey } = state + let i = indexOf(orderKey, state.current.id) + let next = (state.welcome || i === orderKey.length - 1) ? 0 : i+1 + next = findIndex(state.codeStore, { id: orderKey[next] }) + return { + ...state, + welcome: false, + codeStore: updateUserCode(state), + current: { + id: state.codeStore[next].id, + code: state.codeStore[next].userCode, + isSolution: false } } - break; - case PREVIOUS_SNIPPET: - for (let i = 0; i < state.codeStore.length; i++) { - if (state.codeStore[i].id === state.current.id) { - let prev; - if (state.welcome) prev = 0; - else prev = i === 0 ? state.codeStore.length - 1 : i-1; - return { - welcome: false, - codeStore: updateCodeStore(state), - current: { - id: state.codeStore[prev].id, - code: state.codeStore[prev].userCode, - isSolution: false - } - } + } + case types.PREVIOUS_SNIPPET: { + let { orderKey } = state + let i = indexOf(orderKey, state.current.id) + let prev = (state.welcome || i === 0) ? orderKey.length - 1 : i-1 + prev = findIndex(state.codeStore, { id: orderKey[prev] }) + return { + ...state, + welcome: false, + codeStore: updateUserCode(state), + current: { + id: state.codeStore[prev].id, + code: state.codeStore[prev].userCode, + isSolution: false } } - break; + } default: - return state; + return state } } + +export default editor diff --git a/src/reducers/modal.js b/src/reducers/modal.js index 6b9af2a..8d81b19 100644 --- a/src/reducers/modal.js +++ b/src/reducers/modal.js @@ -1,23 +1,54 @@ -import { OPEN_MODAL, CLOSE_MODAL } from '../actions/modal'; +import * as types from '../actions/types' +import { CODE } from '../assets/codeRef' const defaultState = { renderModal: false, - modalId: '' -}; + modalId: '', + messages: [], + subHeader: '', + modalType: '' +} export default (state = defaultState, action) => { switch (action.type) { - case CLOSE_MODAL: + case types.CLOSE_MODAL: return { ...state, renderModal: false - }; - case OPEN_MODAL: + } + case types.OPEN_RESOURCES_MODAL: + // only toggle rederModal if + // modal state already loaded + if (state.modalId === action.id) { + return { + ...state, + renderModal: true + } + } + // otherwise find the right + // modal and load it's state + for (let category in CODE) { + for (let topic of CODE[category]) { + if (topic.title === action.id) { + return { + modalId: action.id, + modalType: 'resources', + renderModal: true, + messages: topic.resources + } + } + } + } + break + case types.OPEN_ANNOUNCEMENT_MODAL: return { + modalId: action.id, + modalType: 'announcement', renderModal: true, - modalId: action.id - }; + subHeader: action.subHeader, + messages: action.messages + } default: - return state; + return state } -}; +} diff --git a/src/reducers/panes.js b/src/reducers/panes.js new file mode 100644 index 0000000..3c7e715 --- /dev/null +++ b/src/reducers/panes.js @@ -0,0 +1,71 @@ +import * as types from '../actions/types' + +const initialState = { + topHeight: '70%', + bottomHeight: '30%', + leftWidth: '30%', + rightWidth: '69.5%', + transition: 'none', + clickState: 0 +} + +let defaultState = JSON.parse( + localStorage.getItem('cs-pg-react-panesState') +) || initialState + +const hidePanes = (state) => { + switch (state.clickState) { + case 0: + return { + ...state, + topHeight: '0%', + bottomHeight: '99%', + transition: '.5s', + clickState: 1 + } + case 1: + return { + ...state, + topHeight: '99%', + bottomHeight: '0%', + transition: '.5s', + clickState: 2 + } + default: + return { + ...state, + topHeight: '70%', + bottomHeight: '30%', + transition: '.5s', + clickState: 0 + } + } +} + +const panes = (state = defaultState, action) => { + switch (action.type) { + case types.RESET_STATE: + localStorage.removeItem('cs-pg-react-panesState') + return initialState + case types.DRAG_HORIZONTAL: + return { + ...state, + leftWidth: action.leftWidth, + rightWidth: action.rightWidth, + transition: 'none' + } + case types.DRAG_VERTICAL: + return { + ...state, + topHeight: action.topHeight, + bottomHeight: action.bottomHeight, + transition: 'none' + } + case types.DOUBLE_CLICK: + return hidePanes(state) + default: + return state + } +} + +export default panes diff --git a/src/reducers/resources.js b/src/reducers/resources.js deleted file mode 100644 index 78a1ff3..0000000 --- a/src/reducers/resources.js +++ /dev/null @@ -1,20 +0,0 @@ -import CODE from '../assets/codeRef'; -import { SELECT_TOPIC } from '../actions/resources'; - -export default (state = [], action) => { - switch (action.type) { - case SELECT_TOPIC: - for (let category in CODE) { - for (let topic of CODE[category]) { - if (topic.title === action.id) { - return [ - ...topic.resources - ]; - } - } - } - break; - default: - return state; - } -}; diff --git a/src/reducers/rootReducer.js b/src/reducers/rootReducer.js index 1ee9abb..50cef20 100644 --- a/src/reducers/rootReducer.js +++ b/src/reducers/rootReducer.js @@ -1,14 +1,14 @@ -import editor from './editor'; -import { combineReducers } from 'redux'; -import consoleOutput from './console'; -import modal from './modal'; -import resources from './resources'; +import { combineReducers } from 'redux' +import editor from './editor' +import console from './console' +import modal from './modal' +import panes from './panes' const rootReducer = combineReducers({ editor, - consoleOutput, + console, modal, - resources -}); + panes +}) -export default rootReducer; +export default rootReducer diff --git a/src/reducers/utils.js b/src/reducers/utils.js new file mode 100644 index 0000000..bf4f100 --- /dev/null +++ b/src/reducers/utils.js @@ -0,0 +1,102 @@ +import _ from 'lodash' + +// STORE INITIALIZATION UTILS: +// codeStore initialization utility +export function populateCodeStore(CODE, arr = []) { + for (let category in CODE) { + _.forEach( + CODE[category], + challenge => + arr.push({ + id: _.replace(challenge.title, /\s/g, ''), + userCode: challenge.seed + }) + ) + } + return arr +} + +// => arr of chal IDs in correct order +export function createOrderKey(CODE) { + const { + SORTING_ALGOS, + DATA_STRUCTURES, + EASY_ALGOS, + MODERATE_ALGOS + } = CODE + return _ + .chain(_ + .flatten([ + SORTING_ALGOS, + DATA_STRUCTURES, + EASY_ALGOS, + MODERATE_ALGOS + ])) + .map(c => + _.replace(c.title, /\s/g, '')) +} + +// MERGE CODE STORE UTILS: +// isolate new challenges, combine, remove exact dupes +function mergeCodeStores({ codeStore: initialState }, { codeStore }) { + return _.uniqWith([ + ...codeStore, + ..._.filter( + initialState, + challenge => _.findIndex( + codeStore, + { id: challenge.id } + ) === -1 + ) + ], _.isEqual) +} + +function removeDuplicates(codeStore) { + const lsKey = 'cs-pg-react-dupes-removed' + if (!localStorage.getItem(lsKey)) { + for (let i = 0; i < codeStore.length; i++) { + if (codeStore[i]) { + const predicate = { id: codeStore[i].id } + while (_.findIndex(codeStore, predicate, i+1) > i) { + const idx = _.findIndex(codeStore, predicate, i+1) + codeStore[idx] = null + } + } + } + localStorage.setItem(lsKey, true) + } + return _.filter( + codeStore, + challenge => challenge !== null + ) +} + +function add_SUPPRESS_TESTS_onlyOnce(codeStore, current, welcome) { + const lsKey = 'cs-pg-react-suppress-tests-only-once' + if (!localStorage.getItem(lsKey)) { + for (let challenge of codeStore) { + challenge.userCode = challenge.userCode.concat( + '\r\r// SUPPRESS TESTS, delete this line to activate\r' + ) + } + if (!current.isSolution && !welcome) { + current.code = current.code.concat( + '\r\r// SUPPRESS TESTS, delete this line to activate\r' + ) + } + localStorage.setItem(lsKey, true) + } + return { codeStore, current } +} + +// compose utils, return dupe free code store +export function composeCodeStore(initialState, defaultState) { + return add_SUPPRESS_TESTS_onlyOnce( + removeDuplicates( + mergeCodeStores(initialState, defaultState), + initialState.codeStore + ), + defaultState.current, + defaultState.welcome + ) +} diff --git a/src/styles/app.css b/src/styles/app.css index abccbfe..5769c9d 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -1,12 +1,10 @@ .main.right-pane { - width: 69.5%; height: 100vh; } .sidebar.left-pane { top: 0; left: 0; - width: 30%; box-sizing: border-box; height: 100vh; flex: auto; @@ -21,6 +19,15 @@ background-color: #a19c9c; background-repeat: no-repeat; background-position: 50%; + transition: background-color .3s; +} + +.divider:hover { + background-color: #279198; +} + +.divider:active { + background-color: #10656b; } .vertical.divider { diff --git a/src/styles/codemirror.css b/src/styles/codemirror.css index 0cc7a1e..dbd9992 100644 --- a/src/styles/codemirror.css +++ b/src/styles/codemirror.css @@ -7,6 +7,10 @@ height: 100% !important; } +.CodeMirror .cm-matchhighlight { + background: rgba(222, 153, 174, 0.3); +} + .CodeMirror div.CodeMirror-cursor { border-left: 1px solid #d5d5d5 !important; } diff --git a/src/styles/console.css b/src/styles/console.css index 1f996dc..4c41edc 100644 --- a/src/styles/console.css +++ b/src/styles/console.css @@ -1,5 +1,4 @@ .sidebar--output.bottom-pane { - height: 30%; margin: 0; position: relative; } @@ -26,17 +25,35 @@ color: #ff0000; } -.sidebar--output--messages--message:last-of-type { - margin-bottom: 5px; -} - .sidebar--output--messages--message { margin: 0; padding: 5px 0 0 8px; white-space: pre; } +.sidebar--output--messages--message:last-of-type { + margin-bottom: 5px; +} + .sidebar--output--messages--default-message { padding: 5px 0 0 8px; color: #787577; } + +.type { + color: rgb(241, 246, 9); +} + +code { + color: #787577; + font-style: italic; +} + +a { + color: #248064; + transition: .5s; +} + +a:hover { + color: #46bc99; +} diff --git a/src/styles/index.css b/src/styles/index.css index 20fb719..fb42ca8 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -2,6 +2,7 @@ body { font-family: Ubuntu; margin: 0; position: relative; + overflow-y: hidden; } #root { diff --git a/src/styles/menu.css b/src/styles/menu.css index 6d8fcb0..5ae5cc8 100644 --- a/src/styles/menu.css +++ b/src/styles/menu.css @@ -1,8 +1,8 @@ .sidebar--menu.top-pane { overflow-y: auto; - height: 70%; flex: auto; user-select: none; + /* transition: .2s ease-in; */ } .sidebar--menu--header { @@ -48,6 +48,7 @@ padding: 5px; margin: 0; transition: .2s !important; + overflow-x: hidden; } .sidebar--menu--detail--button { diff --git a/src/styles/modal.css b/src/styles/modal.css index 17c9184..cb8f77d 100644 --- a/src/styles/modal.css +++ b/src/styles/modal.css @@ -1,3 +1,7 @@ +#modal-root { + z-index: 4; +} + .modal { display: block; background: #5b5b5b; @@ -12,11 +16,16 @@ margin: 5px 0 5px 0; } -.modal a { +.modal a, +.modal span { text-decoration: none; color: #46bc99; } +.modal code { + color: rgb(61, 58, 62) +} + .modal a:hover { text-decoration: underline; color: #99cc99; diff --git a/src/utils/base64.js b/src/utils/base64.js index 44b1417..7e81bce 100644 --- a/src/utils/base64.js +++ b/src/utils/base64.js @@ -1,5 +1,5 @@ -export const VERTICAL_GRIP = 'url(\'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg==\')'; -export const HORIZONTAL_GRIP = 'url(\'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII=\')'; -export const ICON_WHITE = `data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTYuMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjE2cHgiIGhlaWdodD0iMTZweCIgdmlld0JveD0iMCAwIDYxMiA2MTIiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDYxMiA2MTI7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPGc+Cgk8ZyBpZD0iYmFja3NwYWNlIj4KCQk8cGF0aCBkPSJNNTYxLDc2LjVIMTc4LjVjLTE3Ljg1LDAtMzAuNiw3LjY1LTQwLjgsMjIuOTVMMCwzMDZsMTM3LjcsMjA2LjU1YzEwLjIsMTIuNzUsMjIuOTUsMjIuOTUsNDAuOCwyMi45NUg1NjEgICAgYzI4LjA1LDAsNTEtMjIuOTUsNTEtNTF2LTM1N0M2MTIsOTkuNDUsNTg5LjA1LDc2LjUsNTYxLDc2LjV6IE00ODQuNSwzOTcuOGwtMzUuNywzNS43TDM1NywzNDEuN2wtOTEuOCw5MS44bC0zNS43LTM1LjcgICAgbDkxLjgtOTEuOGwtOTEuOC05MS44bDM1LjctMzUuN2w5MS44LDkxLjhsOTEuOC05MS44bDM1LjcsMzUuN0wzOTIuNywzMDZMNDg0LjUsMzk3Ljh6IiBmaWxsPSIjY2JjYmNiIi8+Cgk8L2c+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPC9zdmc+Cg==`; -export const ICON_BLACK = `data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTYuMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjE2cHgiIGhlaWdodD0iMTZweCIgdmlld0JveD0iMCAwIDYxMiA2MTIiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDYxMiA2MTI7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPGc+Cgk8ZyBpZD0iYmFja3NwYWNlIj4KCQk8cGF0aCBkPSJNNTYxLDc2LjVIMTc4LjVjLTE3Ljg1LDAtMzAuNiw3LjY1LTQwLjgsMjIuOTVMMCwzMDZsMTM3LjcsMjA2LjU1YzEwLjIsMTIuNzUsMjIuOTUsMjIuOTUsNDAuOCwyMi45NUg1NjEgICAgYzI4LjA1LDAsNTEtMjIuOTUsNTEtNTF2LTM1N0M2MTIsOTkuNDUsNTg5LjA1LDc2LjUsNTYxLDc2LjV6IE00ODQuNSwzOTcuOGwtMzUuNywzNS43TDM1NywzNDEuN2wtOTEuOCw5MS44bC0zNS43LTM1LjcgICAgbDkxLjgtOTEuOGwtOTEuOC05MS44bDM1LjctMzUuN2w5MS44LDkxLjhsOTEuOC05MS44bDM1LjcsMzUuN0wzOTIuNywzMDZMNDg0LjUsMzk3Ljh6IiBmaWxsPSIjMWIxZDFhIi8+Cgk8L2c+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPC9zdmc+Cg==`; -export const ICON_DISABLED = `data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTYuMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjE2cHgiIGhlaWdodD0iMTZweCIgdmlld0JveD0iMCAwIDYxMiA2MTIiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDYxMiA2MTI7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPGc+Cgk8ZyBpZD0iYmFja3NwYWNlIj4KCQk8cGF0aCBkPSJNNTYxLDc2LjVIMTc4LjVjLTE3Ljg1LDAtMzAuNiw3LjY1LTQwLjgsMjIuOTVMMCwzMDZsMTM3LjcsMjA2LjU1YzEwLjIsMTIuNzUsMjIuOTUsMjIuOTUsNDAuOCwyMi45NUg1NjEgICAgYzI4LjA1LDAsNTEtMjIuOTUsNTEtNTF2LTM1N0M2MTIsOTkuNDUsNTg5LjA1LDc2LjUsNTYxLDc2LjV6IE00ODQuNSwzOTcuOGwtMzUuNywzNS43TDM1NywzNDEuN2wtOTEuOCw5MS44bC0zNS43LTM1LjcgICAgbDkxLjgtOTEuOGwtOTEuOC05MS44bDM1LjctMzUuN2w5MS44LDkxLjhsOTEuOC05MS44bDM1LjcsMzUuN0wzOTIuNywzMDZMNDg0LjUsMzk3Ljh6IiBmaWxsPSIjNzg3NTc3Ii8+Cgk8L2c+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPC9zdmc+Cg==`; +export const VERTICAL_GRIP = 'url(\'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg==\')' +export const HORIZONTAL_GRIP = 'url(\'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII=\')' +export const ICON_WHITE = `data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTYuMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjE2cHgiIGhlaWdodD0iMTZweCIgdmlld0JveD0iMCAwIDYxMiA2MTIiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDYxMiA2MTI7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPGc+Cgk8ZyBpZD0iYmFja3NwYWNlIj4KCQk8cGF0aCBkPSJNNTYxLDc2LjVIMTc4LjVjLTE3Ljg1LDAtMzAuNiw3LjY1LTQwLjgsMjIuOTVMMCwzMDZsMTM3LjcsMjA2LjU1YzEwLjIsMTIuNzUsMjIuOTUsMjIuOTUsNDAuOCwyMi45NUg1NjEgICAgYzI4LjA1LDAsNTEtMjIuOTUsNTEtNTF2LTM1N0M2MTIsOTkuNDUsNTg5LjA1LDc2LjUsNTYxLDc2LjV6IE00ODQuNSwzOTcuOGwtMzUuNywzNS43TDM1NywzNDEuN2wtOTEuOCw5MS44bC0zNS43LTM1LjcgICAgbDkxLjgtOTEuOGwtOTEuOC05MS44bDM1LjctMzUuN2w5MS44LDkxLjhsOTEuOC05MS44bDM1LjcsMzUuN0wzOTIuNywzMDZMNDg0LjUsMzk3Ljh6IiBmaWxsPSIjY2JjYmNiIi8+Cgk8L2c+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPC9zdmc+Cg==` +export const ICON_BLACK = `data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTYuMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjE2cHgiIGhlaWdodD0iMTZweCIgdmlld0JveD0iMCAwIDYxMiA2MTIiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDYxMiA2MTI7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPGc+Cgk8ZyBpZD0iYmFja3NwYWNlIj4KCQk8cGF0aCBkPSJNNTYxLDc2LjVIMTc4LjVjLTE3Ljg1LDAtMzAuNiw3LjY1LTQwLjgsMjIuOTVMMCwzMDZsMTM3LjcsMjA2LjU1YzEwLjIsMTIuNzUsMjIuOTUsMjIuOTUsNDAuOCwyMi45NUg1NjEgICAgYzI4LjA1LDAsNTEtMjIuOTUsNTEtNTF2LTM1N0M2MTIsOTkuNDUsNTg5LjA1LDc2LjUsNTYxLDc2LjV6IE00ODQuNSwzOTcuOGwtMzUuNywzNS43TDM1NywzNDEuN2wtOTEuOCw5MS44bC0zNS43LTM1LjcgICAgbDkxLjgtOTEuOGwtOTEuOC05MS44bDM1LjctMzUuN2w5MS44LDkxLjhsOTEuOC05MS44bDM1LjcsMzUuN0wzOTIuNywzMDZMNDg0LjUsMzk3Ljh6IiBmaWxsPSIjMWIxZDFhIi8+Cgk8L2c+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPC9zdmc+Cg==` +export const ICON_DISABLED = `data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTYuMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjE2cHgiIGhlaWdodD0iMTZweCIgdmlld0JveD0iMCAwIDYxMiA2MTIiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDYxMiA2MTI7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPGc+Cgk8ZyBpZD0iYmFja3NwYWNlIj4KCQk8cGF0aCBkPSJNNTYxLDc2LjVIMTc4LjVjLTE3Ljg1LDAtMzAuNiw3LjY1LTQwLjgsMjIuOTVMMCwzMDZsMTM3LjcsMjA2LjU1YzEwLjIsMTIuNzUsMjIuOTUsMjIuOTUsNDAuOCwyMi45NUg1NjEgICAgYzI4LjA1LDAsNTEtMjIuOTUsNTEtNTF2LTM1N0M2MTIsOTkuNDUsNTg5LjA1LDc2LjUsNTYxLDc2LjV6IE00ODQuNSwzOTcuOGwtMzUuNywzNS43TDM1NywzNDEuN2wtOTEuOCw5MS44bC0zNS43LTM1LjcgICAgbDkxLjgtOTEuOGwtOTEuOC05MS44bDM1LjctMzUuN2w5MS44LDkxLjhsOTEuOC05MS44bDM1LjcsMzUuN0wzOTIuNywzMDZMNDg0LjUsMzk3Ljh6IiBmaWxsPSIjNzg3NTc3Ii8+Cgk8L2c+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPC9zdmc+Cg==` diff --git a/src/utils/editorConfig.js b/src/utils/editorConfig.js index d956161..84bfc76 100644 --- a/src/utils/editorConfig.js +++ b/src/utils/editorConfig.js @@ -5,14 +5,35 @@ export default { matchBrackets: true, styleActiveLine: true, autoCloseBrackets: true, + highlightSelectionMatches: true, theme: 'tomorrow-night-eighties', mode: "javascript", keyMap: 'sublime', + gutters: [ + 'CodeMirror-linenumbers', + 'CodeMirror-foldgutter', + 'CodeMirror-lint-markers' + ], lint: { - esversion: 6 + // allow ES6 syntax + esversion: 6, + // suppress multi-line ternary warnings + laxbreak: true, + // suppress missing semi-colon warnings + asi: true }, - gutters: [ - 'CodeMirror-linenumbers', - 'CodeMirror-foldgutter' - ] -}; + extraKeys: { + // prevent default + 'Ctrl-Enter': () => { + return false + }, + // prevent default + 'Cmd-Enter': () => { + return false + }, + // prevent default + 'Ctrl-Space': (cm) => { + cm.showHint() + } + } +} diff --git a/src/utils/regexp.js b/src/utils/regexp.js new file mode 100644 index 0000000..d825b4f --- /dev/null +++ b/src/utils/regexp.js @@ -0,0 +1,5 @@ +// regexp constructors +export const RESET_STATE = new RegExp('resetState\\(\\)') +export const SUPPRESS_TESTS = new RegExp('\\/\\/\\s\\s?SUPPRESS\\s\\s?TESTS', 'i') +export const DO_NOT_SAVE = new RegExp('\\/\\/\\s\\s?DO\\s\\s?NOT\\s\\s?SAVE', 'i') +export const ERROR_TYPES = new RegExp('tests failed|WARNING:|Fail:|AssertionError|InternalError|RangeError|ReferenceError|EvalError|SyntaxError|TypeError|URIError') diff --git a/src/utils/registerServiceWorker.js b/src/utils/registerServiceWorker.js index 12542ba..7c28f98 100644 --- a/src/utils/registerServiceWorker.js +++ b/src/utils/registerServiceWorker.js @@ -16,30 +16,30 @@ const isLocalhost = Boolean( window.location.hostname.match( /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ ) -); +) export default function register() { if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { // The URL constructor is available in all browsers that support SW. - const publicUrl = new URL(process.env.PUBLIC_URL, window.location); + const publicUrl = new URL(process.env.PUBLIC_URL, window.location) if (publicUrl.origin !== window.location.origin) { // Our service worker won't work if PUBLIC_URL is on a different origin // from what our page is served on. This might happen if a CDN is used to - // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 - return; + // serve assets see https://github.com/facebookincubator/create-react-app/issues/2374 + return } window.addEventListener('load', () => { - const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; + const swUrl = `${process.env.PUBLIC_URL}/service-worker.js` if (isLocalhost) { // This is running on localhost. Lets check if a service worker still exists or not. - checkValidServiceWorker(swUrl); + checkValidServiceWorker(swUrl) } else { // Is not local host. Just register service worker - registerValidSW(swUrl); + registerValidSW(swUrl) } - }); + }) } } @@ -48,28 +48,28 @@ function registerValidSW(swUrl) { .register(swUrl) .then(registration => { registration.onupdatefound = () => { - const installingWorker = registration.installing; + const installingWorker = registration.installing installingWorker.onstatechange = () => { if (installingWorker.state === 'installed') { if (navigator.serviceWorker.controller) { // At this point, the old content will have been purged and // the fresh content will have been added to the cache. // It's the perfect time to display a "New content is - // available; please refresh." message in your web app. - console.log('New content is available; please refresh.'); + // available please refresh." message in your web app. + console.log('New content is available please refresh.') } else { // At this point, everything has been precached. // It's the perfect time to display a // "Content is cached for offline use." message. - console.log('Content is cached for offline use.'); + console.log('Content is cached for offline use.') } } - }; - }; + } + } }) .catch(error => { - console.error('Error during service worker registration:', error); - }); + console.error('Error during service worker registration:', error) + }) } function checkValidServiceWorker(swUrl) { @@ -84,25 +84,25 @@ function checkValidServiceWorker(swUrl) { // No service worker found. Probably a different app. Reload the page. navigator.serviceWorker.ready.then(registration => { registration.unregister().then(() => { - window.location.reload(); - }); - }); + window.location.reload() + }) + }) } else { // Service worker found. Proceed as normal. - registerValidSW(swUrl); + registerValidSW(swUrl) } }) .catch(() => { console.log( 'No internet connection found. App is running in offline mode.' - ); - }); + ) + }) } export function unregister() { if ('serviceWorker' in navigator) { navigator.serviceWorker.ready.then(registration => { - registration.unregister(); - }); + registration.unregister() + }) } } diff --git a/src/utils/resize.js b/src/utils/resize.js deleted file mode 100644 index c63e448..0000000 --- a/src/utils/resize.js +++ /dev/null @@ -1,69 +0,0 @@ -import { HORIZONTAL_GRIP, VERTICAL_GRIP } from './base64'; -import styleListeners from './styleListeners'; - -export default ( - leftPane, - topPane, - rightPane, - bottomPane, - verticalDivider, - horizontalDivider -) => { - const leftThreshold = 30, rightThreshold = 70; - - verticalDivider.sdrag(function(el, pageX, startX, pageY, startY, resize) { - resize.skipX = true; - - if (pageX < window.innerWidth * leftThreshold / 100) { - pageX = window.innerWidth * leftThreshold / 100; - resize.pageX = pageX; - } - if (pageX > window.innerWidth * rightThreshold / 100) { - pageX = window.innerWidth * rightThreshold / 100; - resize.pageX = pageX; - } - - let cur = pageX / window.innerWidth * 100; - if (cur < 0) { - cur = 0; - } - if (cur > window.innerWidth) { - cur = window.innerWidth; - } - - const right = 100 - cur - .5; - leftPane.style.width = cur + "%"; - rightPane.style.width = right + "%"; - }, null, "horizontal"); - - horizontalDivider.sdrag(function(el, pageX, startX, pageY, startY, resize) { - resize.skipY = true; - - if (pageY < window.innerHeight * leftThreshold / 100) { - pageY = window.innerHeight * leftThreshold / 100; - resize.pageY = pageY; - } - if (pageY > window.innerHeight * rightThreshold / 100) { - pageY = window.innerHeight * rightThreshold / 100; - resize.pageY = pageY; - } - - let cur = pageY / window.innerHeight * 100; - if (cur < 0) { - cur = 0; - } - if (cur > window.innerHeight) { - cur = window.innerHeight; - } - - const bottom = 100 - cur - 1; - topPane.style.height = cur + "%"; - bottomPane.style.height = bottom + "%"; - }, null, "vertical"); - - // initialize grip styles: - horizontalDivider.style.backgroundImage = HORIZONTAL_GRIP; - verticalDivider.style.backgroundImage = VERTICAL_GRIP; - - styleListeners(horizontalDivider, verticalDivider); -}; diff --git a/src/utils/simpleDrag.js b/src/utils/simpleDrag.js index ab77ed9..1cc68d0 100644 --- a/src/utils/simpleDrag.js +++ b/src/utils/simpleDrag.js @@ -6,20 +6,20 @@ export default function() { * Howto * ============ * - * document.getElementById('my_target').sdrag(); + * document.getElementById('my_target').simpleDrag() * * onDrag, onStop * ------------------- - * document.getElementById('my_target').sdrag(onDrag, null); - * document.getElementById('my_target').sdrag(null, onStop); - * document.getElementById('my_target').sdrag(onDrag, onStop); + * document.getElementById('my_target').simpleDrag(onDrag, null) + * document.getElementById('my_target').simpleDrag(null, onStop) + * document.getElementById('my_target').simpleDrag(onDrag, onStop) * * Both onDrag and onStop callback take the following arguments: * * - el, the currentTarget element (#my_target in the above examples) * - pageX: the mouse event's pageX property (horizontal position of the mouse compared to the viewport) * - startX: the distance from the element's left property to the horizontal mouse position in the viewport. - * Usually, you don't need to use that property; it is internally used to fix the undesirable + * Usually, you don't need to use that property it is internally used to fix the undesirable * offset that naturally occurs when you don't drag the element by its top left corner * (for instance if you drag the element from its center). * - pageY: the mouse event's pageX property (horizontal position of the mouse compared to the viewport) @@ -66,67 +66,67 @@ export default function() { */ // simple drag - function sdrag(onDrag, onStop, direction) { + function simpleDrag(onDrag, onStop, direction) { - var startX = 0; - var startY = 0; - var el = this; - var dragging = false; + var startX = 0 + var startY = 0 + var el = this + var dragging = false function move(e) { - var fix = {}; - onDrag && onDrag(el, e.pageX, startX, e.pageY, startY, fix); + var fix = {} + onDrag && onDrag(el, e.pageX, startX, e.pageY, startY, fix) if ('vertical' !== direction) { var pageX = ('pageX' in fix) ? fix.pageX - : e.pageX; + : e.pageX if ('startX' in fix) { - startX = fix.startX; + startX = fix.startX } if (false === ('skipX' in fix)) { - el.style.left = (pageX - startX) + 'px'; + el.style.left = (pageX - startX) + 'px' } } if ('horizontal' !== direction) { var pageY = ('pageY' in fix) ? fix.pageY - : e.pageY; + : e.pageY if ('startY' in fix) { - startY = fix.startY; + startY = fix.startY } if (false === ('skipY' in fix)) { - el.style.top = (pageY - startY) + 'px'; + el.style.top = (pageY - startY) + 'px' } } } function startDragging(e) { if (e.currentTarget instanceof HTMLElement || e.currentTarget instanceof SVGElement) { - dragging = true; + dragging = true var left = el.style.left ? parseInt(el.style.left, 10) - : 0; + : 0 var top = el.style.top ? parseInt(el.style.top, 10) - : 0; - startX = e.pageX - left; - startY = e.pageY - top; - window.addEventListener('mousemove', move); + : 0 + startX = e.pageX - left + startY = e.pageY - top + window.addEventListener('mousemove', move) } else { - throw new Error("Your target must be an html element"); + throw new Error("Your target must be an html element") } } - this.addEventListener('mousedown', startDragging); + this.addEventListener('mousedown', startDragging) window.addEventListener('mouseup', function(e) { if (true === dragging) { - dragging = false; - window.removeEventListener('mousemove', move); - onStop && onStop(el, e.pageX, startX, e.pageY, startY); + dragging = false + window.removeEventListener('mousemove', move) + onStop && onStop(el, e.pageX, startX, e.pageY, startY) } - }); + }) } - Element.prototype.sdrag = sdrag; -}; + Element.prototype.simpleDrag = simpleDrag +} diff --git a/src/utils/styleListeners.js b/src/utils/styleListeners.js deleted file mode 100644 index cedb39e..0000000 --- a/src/utils/styleListeners.js +++ /dev/null @@ -1,29 +0,0 @@ -import { HORIZONTAL_GRIP, VERTICAL_GRIP } from './base64'; - -export default (horizontalDivider, verticalDivider) => { - verticalDivider.addEventListener('mousedown', function(e) { - e.target.style.background = '#279198'; - e.target.style.backgroundImage = VERTICAL_GRIP; - e.target.style.backgroundRepeat = 'no-repeat'; - e.target.style.backgroundPosition = '50%'; - }); - - horizontalDivider.addEventListener('mousedown', function(e) { - e.target.style.background = '#279198'; - e.target.style.backgroundImage = HORIZONTAL_GRIP; - e.target.style.backgroundRepeat = 'no-repeat'; - e.target.style.backgroundPosition = '50%'; - }); - - document.addEventListener('mouseup', function(e) { - verticalDivider.style.background = '#a19c9c'; - verticalDivider.style.backgroundImage = VERTICAL_GRIP; - verticalDivider.style.backgroundRepeat = 'no-repeat'; - verticalDivider.style.backgroundPosition = '50%'; - - horizontalDivider.style.background = '#a19c9c'; - horizontalDivider.style.backgroundImage = HORIZONTAL_GRIP; - horizontalDivider.style.backgroundRepeat = 'no-repeat'; - horizontalDivider.style.backgroundPosition = '50%'; - }); -} diff --git a/src/utils/test/app/create-jest-test.js b/src/utils/test/app/create-jest-test.js new file mode 100644 index 0000000..20665ee --- /dev/null +++ b/src/utils/test/app/create-jest-test.js @@ -0,0 +1,18 @@ +import { concatTests, logResults } from './jest-test-utils' +import TESTS from '../../../assets/testRef' +import { SOLUTIONS } from '../../../assets/codeRef' + +export default (ID) => { + test(ID, () => { + // eslint-disable-next-line + const { passed, results } = eval( + concatTests( + SOLUTIONS[ID], + TESTS[ID].tail ? TESTS[ID].tail : '', + JSON.stringify(TESTS[ID].tests) + ) + ) + logResults(passed, results, ID) + expect(passed).toBe(true) + }) +} diff --git a/src/utils/test/app/jest-test-scripts.js b/src/utils/test/app/jest-test-scripts.js new file mode 100644 index 0000000..6a9339a --- /dev/null +++ b/src/utils/test/app/jest-test-scripts.js @@ -0,0 +1,53 @@ +/* eslint-disable no-eval */ + +// stringify functions, concat in correct order and +// pass to eval for crappy way to test solution code +const suppressConsole = () => ({ + log: (arg) => { + if (typeof arg === 'string' && + (arg.includes('Pass:') || !arg.includes('Fail:')) + ) { + return arg + } + return null + } +}) + +function executeTests(tests, hook = {}) { + let passed = true + const results = [] + /* eslint-disable no-unused-vars */ + const isTestDisabled = require('../common/is-test-disabled') + const { invoke, forEach } = require('lodash') + const assert = require('assert') + /* eslint-enable no-unused-vars */ + if (tests) { + invoke(hook, 'beforeAll') + forEach(tests, test => { + try { + invoke(hook, 'beforeEach') + if (test.method) { + // assert w/ method + assert[test.method]( + eval(test.expression), + test.expected, + test.message + ) + } else { + // assert w/o method + assert(eval(test.expression), test.message) + } + invoke(hook, 'afterEach') + results.push('Pass: ' + test.message) + } catch (e) { + results.push('Fail: ' + test.message) + passed = false + } + }) + } + invoke(hook, 'afterAll') + return { passed, results } +} + +export const __suppressConsole__ = suppressConsole.toString() +export const __executeTests__ = executeTests.toString() diff --git a/src/utils/test/app/jest-test-utils.js b/src/utils/test/app/jest-test-utils.js new file mode 100644 index 0000000..9ad3843 --- /dev/null +++ b/src/utils/test/app/jest-test-utils.js @@ -0,0 +1,42 @@ +import chalk from 'chalk' + +import { + __executeTests__, + __suppressConsole__ +} from './jest-test-scripts' + +export const concatTests = ( + solution, + tail, + tests +) => { + return ` + // suppress logs during tests + const console = (${__suppressConsole__})(); + // execute tests + (() => { + ${solution} + ${tail} + const tests = ${tests}; + ${__executeTests__} + return typeof testHooks !== 'undefined' + ? executeTests(tests, testHooks) + : executeTests(tests); + })(); + ` +} + +export const logResults = (passed, results, id) => { + if (!passed) { + // LOGS ONLY FAILING SUITES + console.log(chalk.keyword('salmon').underline(id + ':')) + results.forEach(t => t[0] === 'F' + ? console.log(chalk.red(t)) + : console.log(t) + ) + } else { + // UNCOMMENT TO SEE ALL RESULTS + // console.log(chalk.keyword('salmon').underline(id + ':')); + // results.forEach(t => console.log(t)) + } +} diff --git a/src/utils/test/challenge/eval-code-run-tests.js b/src/utils/test/challenge/eval-code-run-tests.js new file mode 100644 index 0000000..9fb8c6c --- /dev/null +++ b/src/utils/test/challenge/eval-code-run-tests.js @@ -0,0 +1,36 @@ +import executeTests from './execute-tests' +import { SUPPRESS_TESTS } from '../../regexp' +import TESTS from '../../../assets/testRef' + +// TODO: remove check for tests once all challenges have tests + +export default (code, id) => { + try { + /* eslint-disable no-unused-vars */ + const assert = require('assert') + const { forEach, invoke } = require('lodash') + const { beginTests, logTestReport } = require('./testReport') + const isTestDisabled = require('../common/is-test-disabled') + /* eslint-enable no-unused-vars */ + + let prepend = 'const tests = ', tail = '', tests = '' + + // if suppressTests is true, only eval code, + // otherwise, eval code and run tests (if tests exist) + if (!SUPPRESS_TESTS.test(code) && TESTS[id]) { + tests = prepend + JSON.stringify(TESTS[id].tests) + if (TESTS[id].tail) tail += TESTS[id].tail + } + + // eslint-disable-next-line + eval( + code + + tail + + tests + + executeTests + ) + + } catch (e) { + console.log(e.toString()) + } +} diff --git a/src/utils/test/challenge/execute-tests.js b/src/utils/test/challenge/execute-tests.js new file mode 100644 index 0000000..c493c95 --- /dev/null +++ b/src/utils/test/challenge/execute-tests.js @@ -0,0 +1,73 @@ +/* eslint-disable no-eval */ +/* eslint-disable no-undef */ +function executeTests(tests, hook = {}) { + if (tests) { + console.log(beginTests) + // init report vars + let numPassed = 0, numDisabled = 0 + // run beforeAll hook + invoke(hook, 'beforeAll') + // iterate tests array + forEach(tests, (test, i) => { + try { + // run beforeEach hook + invoke(hook, 'beforeEach') + // eval to prevent further execution if disabled + if (eval(test.expression) === 'DISABLED') { + numDisabled++ + throw new Error('DISABLED') + } else { + // run beforeEach again since test + // expression has already been evaled + invoke(hook, 'beforeEach') + // test enabled + if (test.method) { + // assert w/ method + assert[test.method]( + eval(test.expression), + test.expected, + test.message + ) + } else { + // assert w/ no method + assert(eval(test.expression), test.message) + } + // run afterEach hook + invoke(hook, 'afterEach') + // log passing test message + console.log('Pass: ' + test.message) + numPassed++ + } + } catch (e) { + // run afterEach hook when test does not pass + invoke(hook, 'afterEach') + // is test disabled? + if (e.message === 'DISABLED') { + // log disabled / greyed out test message + console.log('' + e.message + ': ' + test.message + '') + } + // else if ( // ONLY FOR DEV TO DEBUG TESTS: + // process.env.NODE_ENV !== 'production' && + // e.message !== test.message + // ) { + // console.log('Fail: ' + e.message) + // } + else { + // log just failure message + console.log('Fail: ' + test.message) + } + } + }) + // run afterAll hook + invoke(hook, 'afterAll') + // report results + logTestReport(numPassed, numDisabled, tests) + } +} + +export default ` +${executeTests} +typeof testHooks !== 'undefined' + ? executeTests(tests, testHooks) + : executeTests(tests) +` diff --git a/src/utils/test/challenge/testReport.js b/src/utils/test/challenge/testReport.js new file mode 100644 index 0000000..b2aac66 --- /dev/null +++ b/src/utils/test/challenge/testReport.js @@ -0,0 +1,12 @@ +const logTestReport = (numPassed, numDisabled, tests) => { + console.log('\nREPORT:') + console.log('‾‾‾‾‾‾‾') + console.log(`- ${numPassed} out of ${tests.length} tests passed`) + console.log(`- ${tests.length - numPassed} out of ${tests.length} tests failed`) + console.log(`- ${numDisabled} test${numDisabled === 1 ? '' : 's'} disabled (define method to enable)`) + console.log('\n/***** TESTS END *****/\n') +} + +const beginTests = '\n/***** TESTS BEGIN *****/\n' + +module.exports = { logTestReport, beginTests } diff --git a/src/utils/test/common/is-test-disabled.js b/src/utils/test/common/is-test-disabled.js new file mode 100644 index 0000000..c771360 --- /dev/null +++ b/src/utils/test/common/is-test-disabled.js @@ -0,0 +1,9 @@ +// disable non-mandatory tests by default +// tests are enable when user defines method in question +module.exports = function(dataStructure, method) { + if (typeof new dataStructure()[method] === 'undefined') { + return true + } + + return false +} diff --git a/yarn.lock b/yarn.lock index a9453b3..a0b742d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -624,7 +624,7 @@ babel-plugin-transform-es2015-computed-properties@^6.22.0: babel-runtime "^6.22.0" babel-template "^6.24.1" -babel-plugin-transform-es2015-destructuring@^6.23.0: +babel-plugin-transform-es2015-destructuring@6.23.0, babel-plugin-transform-es2015-destructuring@^6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d" dependencies: @@ -871,13 +871,14 @@ babel-preset-jest@^20.0.3: dependencies: babel-plugin-jest-hoist "^20.0.3" -babel-preset-react-app@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/babel-preset-react-app/-/babel-preset-react-app-3.1.0.tgz#d77f6061ab9d7bf4b3cdc86b7cde9ded0df03e48" +babel-preset-react-app@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/babel-preset-react-app/-/babel-preset-react-app-3.1.1.tgz#d3f06a79742f0e89d7afcb72e282d9809c850920" dependencies: babel-plugin-dynamic-import-node "1.1.0" babel-plugin-syntax-dynamic-import "6.18.0" babel-plugin-transform-class-properties "6.24.1" + babel-plugin-transform-es2015-destructuring "6.23.0" babel-plugin-transform-object-rest-spread "6.26.0" babel-plugin-transform-react-constant-elements "6.23.0" babel-plugin-transform-react-jsx "6.24.1" @@ -1286,7 +1287,7 @@ chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0, chalk@^2.1.0: +chalk@^2.0.0, chalk@^2.1.0, chalk@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba" dependencies: @@ -1860,9 +1861,9 @@ detect-node@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.3.tgz#a2033c09cc8e158d37748fbde7507832bd6ce127" -detect-port-alt@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/detect-port-alt/-/detect-port-alt-1.1.3.tgz#a4d2f061d757a034ecf37c514260a98750f2b131" +detect-port-alt@1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/detect-port-alt/-/detect-port-alt-1.1.5.tgz#a1aa8fc805a4a5df9b905b7ddc7eed036bcce889" dependencies: address "^1.0.1" debug "^2.6.0" @@ -1976,6 +1977,10 @@ dot-prop@^3.0.0: dependencies: is-obj "^1.0.0" +dotenv-expand@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-4.0.1.tgz#68fddc1561814e0a10964111057ff138ced7d7a8" + dotenv@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d" @@ -2165,9 +2170,9 @@ escope@^3.6.0: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-config-react-app@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-2.0.1.tgz#fd0503da01ae608f0c6ae8861de084975142230e" +eslint-config-react-app@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-2.1.0.tgz#23c909f71cbaff76b945b831d2d814b8bde169eb" eslint-import-resolver-node@^0.3.1: version "0.3.1" @@ -3863,6 +3868,10 @@ jsx-ast-utils@^2.0.0: dependencies: array-includes "^3.0.3" +killable@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.0.tgz#da8b84bd47de5395878f95d64d02f2449fe05e6b" + kind-of@^3.0.2: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -4442,7 +4451,13 @@ onetime@^2.0.0: dependencies: mimic-fn "^1.0.0" -opn@5.1.0, opn@^5.1.0: +opn@5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/opn/-/opn-5.2.0.tgz#71fdf934d6827d676cecbea1531f95d354641225" + dependencies: + is-wsl "^1.1.0" + +opn@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/opn/-/opn-5.1.0.tgz#72ce2306a17dbea58ff1041853352b4a8fc77519" dependencies: @@ -5156,41 +5171,41 @@ react-codemirror2@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/react-codemirror2/-/react-codemirror2-3.0.7.tgz#d5d9888158263ae56da766539d7803486566ab9f" -react-dev-utils@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-4.2.0.tgz#d33242bdad9ec502e547bb73597c3860c8b47879" +react-dev-utils@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-5.0.0.tgz#425ac7c9c40c2603bc4f7ab8836c1406e96bb473" dependencies: address "1.0.3" babel-code-frame "6.26.0" chalk "1.1.3" cross-spawn "5.1.0" - detect-port-alt "1.1.3" + detect-port-alt "1.1.5" escape-string-regexp "1.0.5" filesize "3.5.11" global-modules "1.0.0" gzip-size "3.0.0" inquirer "3.3.0" is-root "1.0.0" - opn "5.1.0" - react-error-overlay "^3.0.0" + opn "5.2.0" + react-error-overlay "^4.0.0" recursive-readdir "2.2.1" shell-quote "1.6.1" sockjs-client "1.1.4" strip-ansi "3.0.1" text-table "0.2.0" -react-dom@16.0.0: - version "16.0.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.0.0.tgz#9cc3079c3dcd70d4c6e01b84aab2a7e34c303f58" +react-dom@^16.0.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.2.0.tgz#69003178601c0ca19b709b33a83369fe6124c044" dependencies: fbjs "^0.8.16" loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.0" -react-error-overlay@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-3.0.0.tgz#c2bc8f4d91f1375b3dad6d75265d51cd5eeaf655" +react-error-overlay@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.0.tgz#d198408a85b4070937a98667f500c832f86bd5d4" react-redux@^5.0.6: version "5.0.6" @@ -5203,23 +5218,24 @@ react-redux@^5.0.6: loose-envify "^1.1.0" prop-types "^15.5.10" -react-scripts@1.0.15: - version "1.0.15" - resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-1.0.15.tgz#c96d2df7acf0f86b1c153e1e7fcf23d44b267446" +react-scripts@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-1.1.0.tgz#0c94b2b2e14cff2dad8919397901b5edebeba511" dependencies: autoprefixer "7.1.6" babel-core "6.26.0" babel-eslint "7.2.3" babel-jest "20.0.3" babel-loader "7.1.2" - babel-preset-react-app "^3.1.0" + babel-preset-react-app "^3.1.1" babel-runtime "6.26.0" case-sensitive-paths-webpack-plugin "2.1.1" chalk "1.1.3" css-loader "0.28.7" dotenv "4.0.0" + dotenv-expand "4.0.1" eslint "4.10.0" - eslint-config-react-app "^2.0.1" + eslint-config-react-app "^2.1.0" eslint-loader "1.9.0" eslint-plugin-flowtype "2.39.1" eslint-plugin-import "2.8.0" @@ -5235,12 +5251,12 @@ react-scripts@1.0.15: postcss-loader "2.0.8" promise "8.0.1" raf "3.4.0" - react-dev-utils "^4.2.0" + react-dev-utils "^5.0.0" style-loader "0.19.0" sw-precache-webpack-plugin "0.11.4" url-loader "0.6.2" webpack "3.8.1" - webpack-dev-server "2.9.3" + webpack-dev-server "2.9.4" webpack-manifest-plugin "1.3.2" whatwg-fetch "2.0.3" optionalDependencies: @@ -5257,9 +5273,9 @@ react-transition-group@^2.2.1: prop-types "^15.5.8" warning "^3.0.0" -react@16.0.0: - version "16.0.0" - resolved "https://registry.yarnpkg.com/react/-/react-16.0.0.tgz#ce7df8f1941b036f02b2cca9dbd0cb1f0e855e2d" +react@16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba" dependencies: fbjs "^0.8.16" loose-envify "^1.1.0" @@ -6458,9 +6474,9 @@ webpack-dev-middleware@^1.11.0: range-parser "^1.0.3" time-stamp "^2.0.0" -webpack-dev-server@2.9.3: - version "2.9.3" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.9.3.tgz#f0554e88d129e87796a6f74a016b991743ca6f81" +webpack-dev-server@2.9.4: + version "2.9.4" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.9.4.tgz#7883e61759c6a4b33e9b19ec4037bd4ab61428d1" dependencies: ansi-html "0.0.7" array-includes "^3.0.3" @@ -6476,6 +6492,7 @@ webpack-dev-server@2.9.3: import-local "^0.1.1" internal-ip "1.2.0" ip "^1.1.5" + killable "^1.0.0" loglevel "^1.4.1" opn "^5.1.0" portfinder "^1.0.9"