From caf5713742f5aebdc3f9bc0557a39946290257c3 Mon Sep 17 00:00:00 2001 From: Jeremy Walker Date: Fri, 9 Feb 2024 18:35:18 +0000 Subject: [PATCH 01/12] Fix incorrect URL (#2399) --- exercises/concept/regular-chatbot/.docs/instructions.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/exercises/concept/regular-chatbot/.docs/instructions.md b/exercises/concept/regular-chatbot/.docs/instructions.md index 01196b361f..779fa40688 100644 --- a/exercises/concept/regular-chatbot/.docs/instructions.md +++ b/exercises/concept/regular-chatbot/.docs/instructions.md @@ -66,13 +66,13 @@ The Chatbot is a really curious software. Even though he can search on the inter Example of Conversation: - **Chatbot**: Hey username, I would like to learn how to code in JavaScript, do you know any cool website where I could learn? -- **User**: I learned a lot from [exercism.com](http://website.com) +- **User**: I learned a lot from [exercism.org](http://exercism.org) Implement the function `getURL()` which is able to return an array with just the link of each website. ```javascript -getURL('I learned a lot from exercism.com'); -// => ["exercism.com"]; +getURL('I learned a lot from exercism.org'); +// => ["exercism.org"]; ``` ## Greet the user From 27677be489957c8f56b1dcbaada80cf23711c00e Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Thu, 15 Feb 2024 14:25:09 +0100 Subject: [PATCH 02/12] `pop-count`: rename to `eliuds-eggs` (#2391) * Rename the `pop-count` exercise to `eliuds-eggs`. * Fix file names --- config.json | 2 +- .../{pop-count => eliuds-eggs}/.docs/instructions.md | 0 .../{pop-count => eliuds-eggs}/.docs/introduction.md | 0 exercises/practice/{pop-count => eliuds-eggs}/.eslintrc | 0 exercises/practice/{pop-count => eliuds-eggs}/.gitignore | 0 .../practice/{pop-count => eliuds-eggs}/.meta/config.json | 6 +++--- .../practice/{pop-count => eliuds-eggs}/.meta/proof.ci.js | 0 .../practice/{pop-count => eliuds-eggs}/.meta/tests.toml | 0 exercises/practice/{pop-count => eliuds-eggs}/.npmrc | 0 exercises/practice/{pop-count => eliuds-eggs}/LICENSE | 0 .../practice/{pop-count => eliuds-eggs}/babel.config.js | 0 .../{pop-count/pop-count.js => eliuds-eggs/eliuds-eggs.js} | 0 .../pop-count.spec.js => eliuds-eggs/eliuds-eggs.spec.js} | 4 ++-- exercises/practice/{pop-count => eliuds-eggs}/package.json | 6 +++--- 14 files changed, 9 insertions(+), 9 deletions(-) rename exercises/practice/{pop-count => eliuds-eggs}/.docs/instructions.md (100%) rename exercises/practice/{pop-count => eliuds-eggs}/.docs/introduction.md (100%) rename exercises/practice/{pop-count => eliuds-eggs}/.eslintrc (100%) rename exercises/practice/{pop-count => eliuds-eggs}/.gitignore (100%) rename exercises/practice/{pop-count => eliuds-eggs}/.meta/config.json (83%) rename exercises/practice/{pop-count => eliuds-eggs}/.meta/proof.ci.js (100%) rename exercises/practice/{pop-count => eliuds-eggs}/.meta/tests.toml (100%) rename exercises/practice/{pop-count => eliuds-eggs}/.npmrc (100%) rename exercises/practice/{pop-count => eliuds-eggs}/LICENSE (100%) rename exercises/practice/{pop-count => eliuds-eggs}/babel.config.js (100%) rename exercises/practice/{pop-count/pop-count.js => eliuds-eggs/eliuds-eggs.js} (100%) rename exercises/practice/{pop-count/pop-count.spec.js => eliuds-eggs/eliuds-eggs.spec.js} (87%) rename exercises/practice/{pop-count => eliuds-eggs}/package.json (83%) diff --git a/config.json b/config.json index fcefcc4315..872ac1d1a8 100644 --- a/config.json +++ b/config.json @@ -2533,7 +2533,7 @@ "difficulty": 7 }, { - "slug": "pop-count", + "slug": "eliuds-eggs", "name": "Eliud's Eggs", "uuid": "2a3ecf62-fb5d-4dac-8369-72e7976a8f57", "practices": [], diff --git a/exercises/practice/pop-count/.docs/instructions.md b/exercises/practice/eliuds-eggs/.docs/instructions.md similarity index 100% rename from exercises/practice/pop-count/.docs/instructions.md rename to exercises/practice/eliuds-eggs/.docs/instructions.md diff --git a/exercises/practice/pop-count/.docs/introduction.md b/exercises/practice/eliuds-eggs/.docs/introduction.md similarity index 100% rename from exercises/practice/pop-count/.docs/introduction.md rename to exercises/practice/eliuds-eggs/.docs/introduction.md diff --git a/exercises/practice/pop-count/.eslintrc b/exercises/practice/eliuds-eggs/.eslintrc similarity index 100% rename from exercises/practice/pop-count/.eslintrc rename to exercises/practice/eliuds-eggs/.eslintrc diff --git a/exercises/practice/pop-count/.gitignore b/exercises/practice/eliuds-eggs/.gitignore similarity index 100% rename from exercises/practice/pop-count/.gitignore rename to exercises/practice/eliuds-eggs/.gitignore diff --git a/exercises/practice/pop-count/.meta/config.json b/exercises/practice/eliuds-eggs/.meta/config.json similarity index 83% rename from exercises/practice/pop-count/.meta/config.json rename to exercises/practice/eliuds-eggs/.meta/config.json index 5f80e7bf45..3d0d8b469e 100644 --- a/exercises/practice/pop-count/.meta/config.json +++ b/exercises/practice/eliuds-eggs/.meta/config.json @@ -4,10 +4,10 @@ ], "files": { "solution": [ - "pop-count.js" + "eliuds-eggs.js" ], "test": [ - "pop-count.spec.js" + "eliuds-eggs.spec.js" ], "example": [ ".meta/proof.ci.js" @@ -15,5 +15,5 @@ }, "blurb": "Help Eliud count the number of eggs in her chicken coop by counting the number of 1 bits in a binary representation.", "source": "Christian Willner, Eric Willigers", - "source_url": "https://forum.exercism.org/t/new-exercise-suggestion-pop-count/7632/5" + "source_url": "https://forum.exercism.org/t/new-exercise-suggestion-eliuds-eggs/7632/5" } diff --git a/exercises/practice/pop-count/.meta/proof.ci.js b/exercises/practice/eliuds-eggs/.meta/proof.ci.js similarity index 100% rename from exercises/practice/pop-count/.meta/proof.ci.js rename to exercises/practice/eliuds-eggs/.meta/proof.ci.js diff --git a/exercises/practice/pop-count/.meta/tests.toml b/exercises/practice/eliuds-eggs/.meta/tests.toml similarity index 100% rename from exercises/practice/pop-count/.meta/tests.toml rename to exercises/practice/eliuds-eggs/.meta/tests.toml diff --git a/exercises/practice/pop-count/.npmrc b/exercises/practice/eliuds-eggs/.npmrc similarity index 100% rename from exercises/practice/pop-count/.npmrc rename to exercises/practice/eliuds-eggs/.npmrc diff --git a/exercises/practice/pop-count/LICENSE b/exercises/practice/eliuds-eggs/LICENSE similarity index 100% rename from exercises/practice/pop-count/LICENSE rename to exercises/practice/eliuds-eggs/LICENSE diff --git a/exercises/practice/pop-count/babel.config.js b/exercises/practice/eliuds-eggs/babel.config.js similarity index 100% rename from exercises/practice/pop-count/babel.config.js rename to exercises/practice/eliuds-eggs/babel.config.js diff --git a/exercises/practice/pop-count/pop-count.js b/exercises/practice/eliuds-eggs/eliuds-eggs.js similarity index 100% rename from exercises/practice/pop-count/pop-count.js rename to exercises/practice/eliuds-eggs/eliuds-eggs.js diff --git a/exercises/practice/pop-count/pop-count.spec.js b/exercises/practice/eliuds-eggs/eliuds-eggs.spec.js similarity index 87% rename from exercises/practice/pop-count/pop-count.spec.js rename to exercises/practice/eliuds-eggs/eliuds-eggs.spec.js index b8019c20da..448446843a 100644 --- a/exercises/practice/pop-count/pop-count.spec.js +++ b/exercises/practice/eliuds-eggs/eliuds-eggs.spec.js @@ -1,6 +1,6 @@ -import { eggCount } from './pop-count'; +import { eggCount } from './eliuds-eggs'; -describe('PopCount', () => { +describe('EliudsEggs', () => { test('0 eggs', () => { const expected = 0; const actual = eggCount(0); diff --git a/exercises/practice/pop-count/package.json b/exercises/practice/eliuds-eggs/package.json similarity index 83% rename from exercises/practice/pop-count/package.json rename to exercises/practice/eliuds-eggs/package.json index 646a7cd37b..8d6e32065f 100644 --- a/exercises/practice/pop-count/package.json +++ b/exercises/practice/eliuds-eggs/package.json @@ -1,6 +1,6 @@ { - "name": "@exercism/javascript-pop-count", - "description": "Exercism practice exercise on pop-count", + "name": "@exercism/javascript-eliuds-eggs", + "description": "Exercism practice exercise on eliuds-eggs", "author": "Katrina Owen", "contributors": [ "Cool-Katt (https://github.com/Cool-Katt)", @@ -12,7 +12,7 @@ "repository": { "type": "git", "url": "https://github.com/exercism/javascript", - "directory": "exercises/practice/pop-count" + "directory": "exercises/practice/eliuds-eggs" }, "devDependencies": { "@babel/core": "^7.23.0", From a5fc4bd3f83242b4d24bc9e108329032420fa0c1 Mon Sep 17 00:00:00 2001 From: Isaac Good Date: Fri, 16 Feb 2024 11:23:23 -0800 Subject: [PATCH 03/12] concept/lasagna: change last totalTimeInMinutes test to use distinct values from the first test to make them harder to confuse (#2405) --- exercises/concept/lasagna/lasagna.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/concept/lasagna/lasagna.spec.js b/exercises/concept/lasagna/lasagna.spec.js index 92c02fe58a..0dcd386ab2 100644 --- a/exercises/concept/lasagna/lasagna.spec.js +++ b/exercises/concept/lasagna/lasagna.spec.js @@ -37,6 +37,6 @@ describe('totalTimeInMinutes', () => { test('calculates the total cooking time', () => { expect(totalTimeInMinutes(1, 5)).toBe(7); expect(totalTimeInMinutes(4, 15)).toBe(23); - expect(totalTimeInMinutes(1, 35)).toBe(37); + expect(totalTimeInMinutes(1, 30)).toBe(32); }); }); From 55a9cbd33e1898ccd2f2c439fc2940c2a8d14ca0 Mon Sep 17 00:00:00 2001 From: ultimatejedi Date: Fri, 23 Feb 2024 05:16:30 -0600 Subject: [PATCH 04/12] Update introduction.md (#2407) --- concepts/recursion/introduction.md | 72 +++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/concepts/recursion/introduction.md b/concepts/recursion/introduction.md index 83d31dab92..28571cd59a 100644 --- a/concepts/recursion/introduction.md +++ b/concepts/recursion/introduction.md @@ -1,3 +1,73 @@ # Introduction -TODO: add introduction for recursion concept +The ability for something to be defined in terms of itself is called recursion. +Recursive functions are functions that call themselves. + +Suppose that you have a function called `recurse`. +This function is recursive if it calls itself inside its body, like this: + +```js +function recurse() { + // ... + recurse(); + // ... +} +``` + +A recursive function usually has a condition to stop calling itself and return a value, known as a _base case_. +If a base case is missing, in most cases, because it will call itself indefinitely, it would be able to run forever. +In reality, in most of those situations, you'll end up with a "StackSize error": an error raised by the runtime because the _stack_ of function calls has grown beyond a predefined limit because each recursive call adds to this _stack_ until it returns (and it doesn't). +The message of this error is `Maximum call stack size exceeded`. + +```js +function recurse() { + if (baseCondition) { + // stop calling itself + //... + } else { + recurse(); + } +} +``` + +Recursive functions often can be used instead of `for` loops for more succinct code. +For example, take a countdown. +Here's the more intuitive `for` loop approach: + +```js +function countDown(fromNumber) { + for (let i = fromNumber; i > 0; i--) { + console.log(i); + } +} + +countDown(3); // 3, 2, 1 in separate lines +``` + +We could solve this using recursion too: + +```js +function countDown(fromNumber) { + console.log(fromNumber); + if (fromNumber > 1) { + countDown(fromNumber - 1); + } +} + +countDown(3); // same result +``` + +Here, our base case is when `fromNumber` is 1, in which case we don't call `countDown` again. + +Apart from just displaying numbers, recursive functions can be used for more complicated procedures, such as keeping a sum or total. + +```js +function sum(n) { + if (n <= 1) { + return n; + } + return n + sum(n - 1); +} + +sum(3); // 6 +``` From b76ea0af5e4793fd00ab7b2bed4985e0170c6c5a Mon Sep 17 00:00:00 2001 From: Brian Lister Date: Thu, 29 Feb 2024 10:47:15 -0600 Subject: [PATCH 05/12] fix: change prize to price (#2408) --- exercises/concept/pizza-order/pizza-order.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/concept/pizza-order/pizza-order.js b/exercises/concept/pizza-order/pizza-order.js index c36d646138..4a8ad8bf2b 100644 --- a/exercises/concept/pizza-order/pizza-order.js +++ b/exercises/concept/pizza-order/pizza-order.js @@ -15,7 +15,7 @@ export function pizzaPrice(pizza, ...extras) { } /** - * Calculate the prize of the total order, given individual orders + * Calculate the price of the total order, given individual orders * * @param {PizzaOrder[]} pizzaOrders a list of pizza orders * @returns {number} the price of the total order From 1d4a4810db4a04ff8e08d5dc0873834190140472 Mon Sep 17 00:00:00 2001 From: Charlie <38315638+Xarlizard@users.noreply.github.com> Date: Thu, 29 Feb 2024 17:51:47 +0100 Subject: [PATCH 06/12] Typo fix (#2410) 'request' function had an error referencing to itself as 'fetch' function --- exercises/concept/translation-service/api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/concept/translation-service/api.js b/exercises/concept/translation-service/api.js index e0284a6ec2..c4ab682f0a 100644 --- a/exercises/concept/translation-service/api.js +++ b/exercises/concept/translation-service/api.js @@ -74,7 +74,7 @@ export class ExternalApi { if (typeof callback !== 'function') { throw new BadRequest( - `Expected callback function when calling fetch(text, callback), actual ${typeof callback}.`, + `Expected callback function when calling request(text, callback), actual ${typeof callback}.`, ); } From 3de4d6b1a49e93d3843cebabd77f664ee98bdcac Mon Sep 17 00:00:00 2001 From: Exercism Bot Date: Tue, 5 Mar 2024 14:45:49 +0000 Subject: [PATCH 07/12] =?UTF-8?q?=F0=9F=A4=96=20Sync=20org-wide=20files=20?= =?UTF-8?q?to=20upstream=20repo=20(#2412)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit More info: https://github.com/exercism/org-wide-files/commit/0c0972d1df4cd18d98c7df316348315b06ef49b4 --- .../workflows/no-important-files-changed.yml | 71 ++++--------------- 1 file changed, 13 insertions(+), 58 deletions(-) diff --git a/.github/workflows/no-important-files-changed.yml b/.github/workflows/no-important-files-changed.yml index 26b068bc46..b940c5991c 100644 --- a/.github/workflows/no-important-files-changed.yml +++ b/.github/workflows/no-important-files-changed.yml @@ -1,68 +1,23 @@ name: No important files changed on: - pull_request: + pull_request_target: types: [opened] branches: [main] + paths: + - "exercises/concept/**" + - "exercises/practice/**" + - "!exercises/*/*/.approaches/**" + - "!exercises/*/*/.articles/**" + - "!exercises/*/*/.docs/**" + - "!exercises/*/*/.meta/**" permissions: pull-requests: write jobs: - no_important_files_changed: - name: No important files changed - runs-on: ubuntu-22.04 - steps: - - name: Checkout code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - - name: Check if important files changed - id: check - run: | - set -exo pipefail - - # fetch a ref to the main branch so we can diff against it - git remote set-branches origin main - git fetch --depth 1 origin main - - for changed_file in $(git diff --diff-filter=M --name-only origin/main); do - if ! echo "$changed_file" | grep --quiet --extended-regexp 'exercises/(practice|concept)' ; then - continue - fi - slug="$(echo "$changed_file" | sed --regexp-extended 's#exercises/[^/]+/([^/]+)/.*#\1#' )" - path_before_slug="$(echo "$changed_file" | sed --regexp-extended "s#(.*)/$slug/.*#\\1#" )" - path_after_slug="$( echo "$changed_file" | sed --regexp-extended "s#.*/$slug/(.*)#\\1#" )" - - if ! [ -f "$path_before_slug/$slug/.meta/config.json" ]; then - # cannot determine if important files changed without .meta/config.json - continue - fi - - # returns 0 if the filter matches, 1 otherwise - # | contains($path_after_slug) - if jq --exit-status \ - --arg path_after_slug "$path_after_slug" \ - '[.files.test, .files.invalidator, .files.editor] | flatten | index($path_after_slug)' \ - "$path_before_slug/$slug/.meta/config.json" \ - > /dev/null; - then - echo "important_files_changed=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - done - - echo "important_files_changed=false" >> "$GITHUB_OUTPUT" - - - name: Suggest to add [no important files changed] - if: steps.check.outputs.important_files_changed == 'true' - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea - with: - github-token: ${{ github.token }} - script: | - const body = "This PR touches files which potentially affect the outcome of the tests of an exercise. This will cause all students' solutions to affected exercises to be re-tested.\n\nIf this PR does **not** affect the result of the test (or, for example, adds an edge case that is not worth rerunning all tests for), **please add the following to the merge-commit message** which will stops student's tests from re-running. Please copy-paste to avoid typos.\n```\n[no important files changed]\n```\n\n For more information, refer to the [documentation](https://exercism.org/docs/building/tracks#h-avoiding-triggering-unnecessary-test-runs). If you are unsure whether to add the message or not, please ping `@exercism/maintainers-admin` in a comment. Thank you!" - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: body - }) + pause: + uses: exercism/github-actions/.github/workflows/check-no-important-files-changed.yml@main + with: + repository: ${{ github.event.pull_request.head.repo.owner.login }}/${{ github.event.pull_request.head.repo.name }} + ref: ${{ github.head_ref }} From 12c896b6df32a47f16d9bb76fac40f9eb49b050b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Mar 2024 11:02:39 +0530 Subject: [PATCH 08/12] Bump actions/setup-node from 3.8.1 to 4.0.2 (#2396) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3.8.1 to 4.0.2. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d...60edb5dd545a775178f52524783378180af0d1f8) --- updated-dependencies: - dependency-name: actions/setup-node dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/action-format.yml | 2 +- .github/workflows/ci.js.yml | 4 ++-- .github/workflows/pr.ci.js.yml | 4 ++-- .github/workflows/verify-code-formatting.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/action-format.yml b/.github/workflows/action-format.yml index 01ae327220..1050293fe0 100644 --- a/.github/workflows/action-format.yml +++ b/.github/workflows/action-format.yml @@ -61,7 +61,7 @@ jobs: git checkout -b "$HEAD_REF" "origin/$HEAD_REF" - name: Use Node.js LTS (18.x) - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 with: node-version: '18' - name: Install project development dependencies diff --git a/.github/workflows/ci.js.yml b/.github/workflows/ci.js.yml index 578ea4df0b..7351a601f0 100644 --- a/.github/workflows/ci.js.yml +++ b/.github/workflows/ci.js.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - name: Use Node.js LTS (18.x) - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 with: node-version: '18' @@ -34,7 +34,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 with: node-version: ${{ matrix.node-version }} diff --git a/.github/workflows/pr.ci.js.yml b/.github/workflows/pr.ci.js.yml index f9c0ddc370..975aef7fe5 100644 --- a/.github/workflows/pr.ci.js.yml +++ b/.github/workflows/pr.ci.js.yml @@ -14,7 +14,7 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - name: Use Node.js LTS (18.x) - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 with: node-version: '18' @@ -40,7 +40,7 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 with: node-version: ${{ matrix.node-version }} diff --git a/.github/workflows/verify-code-formatting.yml b/.github/workflows/verify-code-formatting.yml index c342986ef3..22abf651bf 100644 --- a/.github/workflows/verify-code-formatting.yml +++ b/.github/workflows/verify-code-formatting.yml @@ -12,7 +12,7 @@ jobs: - name: 'Checkout code' uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - name: Use Node.js LTS (18.x) - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 with: node-version: '18' - name: Install project development dependencies From c642a12622cdf51933fb6ade7efc0f024103c52e Mon Sep 17 00:00:00 2001 From: Tejas Bubane Date: Fri, 22 Mar 2024 11:35:58 +0100 Subject: [PATCH 09/12] Ignore org-wide files from prettier check (#2422) --- .prettierignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.prettierignore b/.prettierignore index 364d93ccef..eb205c2108 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,6 +6,10 @@ exercises/**/README.md # Originates from https://github.com/exercism/org-wide-files CODE_OF_CONDUCT.md LICENSE +.github/workflows/configlet.yml +.github/workflows/sync-labels.yml +.github/workflows/no-important-files-changed.yml +.github/workflows/pause-community-contributions.yml # These are formatted via configlet and will not match prettier exercises/**/.meta/config.json @@ -14,4 +18,4 @@ config.json # Originates from https://github.com/exercism/problem-specifications exercises/practice/**/.docs/instructions.md -exercises/practice/**/.docs/introduction.md \ No newline at end of file +exercises/practice/**/.docs/introduction.md From fce9cb5481261f55b8c5953e39d63e6bf5c5efb0 Mon Sep 17 00:00:00 2001 From: Cool-Katt Date: Fri, 22 Mar 2024 12:36:25 +0200 Subject: [PATCH 10/12] hotfix for asynchronous tests (#2421) * hotfix for asynchronous tests * Forgot to skip tests, oops... * Revert "Forgot to skip tests, oops..." This reverts commit 908f21604f7530ca8f805c8c070440eaa06b5575. * messed up the tests, again... --- .../workflows/no-important-files-changed.yml | 12 +-- .../.docs/instructions.append.md | 77 ++++++++++++++++--- .../.meta/config.json | 4 + .../.meta/proof.ci.js | 60 +++++++++++---- .../parallel-letter-frequency.js | 2 +- .../parallel-letter-frequency.spec.js | 52 ++++++------- 6 files changed, 150 insertions(+), 57 deletions(-) diff --git a/.github/workflows/no-important-files-changed.yml b/.github/workflows/no-important-files-changed.yml index b940c5991c..72acd358a2 100644 --- a/.github/workflows/no-important-files-changed.yml +++ b/.github/workflows/no-important-files-changed.yml @@ -5,12 +5,12 @@ on: types: [opened] branches: [main] paths: - - "exercises/concept/**" - - "exercises/practice/**" - - "!exercises/*/*/.approaches/**" - - "!exercises/*/*/.articles/**" - - "!exercises/*/*/.docs/**" - - "!exercises/*/*/.meta/**" + - 'exercises/concept/**' + - 'exercises/practice/**' + - '!exercises/*/*/.approaches/**' + - '!exercises/*/*/.articles/**' + - '!exercises/*/*/.docs/**' + - '!exercises/*/*/.meta/**' permissions: pull-requests: write diff --git a/exercises/practice/parallel-letter-frequency/.docs/instructions.append.md b/exercises/practice/parallel-letter-frequency/.docs/instructions.append.md index 0fb61d1eb5..71bf279275 100644 --- a/exercises/practice/parallel-letter-frequency/.docs/instructions.append.md +++ b/exercises/practice/parallel-letter-frequency/.docs/instructions.append.md @@ -1,8 +1,17 @@ # Instructions append -Due to the single-threaded nature of Javascript, code that appears to execute in parallel, -such as `async functions` or `Promises`, actually execute concurrently. -Such solutions will pass all the tests, even though they do not meet the requrement for parallel execution. +Javascript is single-threaded by nature, so it lacks many of the language features that other languages have in order to handle parallel code execution. +In fact, the only way to achieve "real" parallel code execution is through `Worker threads` (also reffered to as `Web Workers`). + +Almost always, code that appears to execute in parallel, +such as `async functions` or `Promises`, will actually execute concurrently instead. +This is often better, since modern Javascript is optimized for such use, +and you will often see code that "emulates" (or "cheats") parallel execution by the use of `Promise.all()` and other concurrent execution methods. + +```exercism/caution +To pass the tests for this exercise, your solution needs to execute _concurrently_ (or in parallel), +meaning that synchronous solutions (e.g. a simple `for` loop) will not pass. +``` ## Concurency vs. Parallelism @@ -11,16 +20,30 @@ Here's a quick definition for each that illustrates the diferences between the t - Concurrency is when two or more tasks can start, run and complete in overlapping time periods, being executed by the same processing unit. - Parallelism is when two or more tasks can start and run at the same time, being executed independently of eachother by separate processing units. +For the sake of completeness, here's a definition for synchronous execution: + +- Synchronous execution is when a task has to wait for another running task to complete, before it can run. + ## Parallelism in Javascript -Even though Javascript by default is single-threaded, there is a way to execute code non-concurently, -through the [Web Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API). +Even though Javascript by default is single-threaded, there is a way to execute code in parallel fashion. +If your running javascript in the browser (e.g. in a web app), +then the way to achieve parallelism is through the [Web Worker API][mdn-demo]. As described by MDN: -> Web Workers makes it possible to run a script operation in a background thread separate from the main execution thread of a web application. +> Web Workers makes it possible to run a script operation in a background thread separate from the main execution thread of an application. + +On the other hand, if your javascript is running in Node.js, which is Exercism's target runtime, +this same concept is known as [Worker threads][node]. + +```exercism/caution +Be aware that the implementation of the worker API differs largely between browsers and other JavaScript environments. -Here's a simple demo (taken from [here](https://medium.com/@ns-tech-learn/what-is-a-web-worker-how-to-use-it-and-example-2273de521f04)) +Make sure to read the documentation for your specific runtime! +``` + +Here's a simple demo of the `Web Worker API` (taken from [here][medium-demo]) ```js // main.js @@ -46,13 +69,49 @@ onmessage = function (event) { }; ``` -As a stretch goal, consider if your implementation can be adapted to make use of `Web workers`. +And here is a demo of the `Worker threads API` (taken from the [docs][node]) + +```js +const { + Worker, + isMainThread, + parentPort, + workerData, +} = require('node:worker_threads'); + +if (isMainThread) { + module.exports = function parseJSAsync(script) { + return new Promise((resolve, reject) => { + const worker = new Worker(__filename, { + workerData: script, + }); + worker.on('message', resolve); + worker.on('error', reject); + worker.on('exit', (code) => { + if (code !== 0) + reject(new Error(`Worker stopped with exit code ${code}`)); + }); + }); + }; +} else { + const { parse } = require('some-js-parsing-library'); + const script = workerData; + parentPort.postMessage(parse(script)); +} +``` + +As a stretch goal, consider if your implementation can be adapted to make use of `Worker threads`. --- ## Further reading -- [MDN demo](https://mdn.github.io/dom-examples/web-workers/simple-web-worker/) +- [Node.js docs](https://nodejs.org/api/worker_threads.html#worker-threads) +- [Another MDN demo](https://mdn.github.io/dom-examples/web-workers/simple-web-worker/) - [MDN - Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) - [Article about multi-threading in JS](https://medium.com/techtrument/multithreading-javascript-46156179cf9a) - [Web Worker primer](https://medium.com/@ns-tech-learn/what-is-a-web-worker-how-to-use-it-and-example-2273de521f04) + +[mdn-demo]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API +[medium-demo]: https://medium.com/@ns-tech-learn/what-is-a-web-worker-how-to-use-it-and-example-2273de521f04 +[node]: https://nodejs.org/api/worker_threads.html#worker-threads diff --git a/exercises/practice/parallel-letter-frequency/.meta/config.json b/exercises/practice/parallel-letter-frequency/.meta/config.json index bfb0b9505d..b2a5c28652 100644 --- a/exercises/practice/parallel-letter-frequency/.meta/config.json +++ b/exercises/practice/parallel-letter-frequency/.meta/config.json @@ -2,6 +2,10 @@ "authors": [ "Cool-Katt" ], + "contributors": [ + "mk-mxp", + "themetar" + ], "files": { "solution": [ "parallel-letter-frequency.js" diff --git a/exercises/practice/parallel-letter-frequency/.meta/proof.ci.js b/exercises/practice/parallel-letter-frequency/.meta/proof.ci.js index 4b6e9bd020..0cf92cd8eb 100644 --- a/exercises/practice/parallel-letter-frequency/.meta/proof.ci.js +++ b/exercises/practice/parallel-letter-frequency/.meta/proof.ci.js @@ -1,18 +1,48 @@ -export const parallelLetterFrequency = (texts) => { - let result = {}; - let formatedTexts = texts - .map((x) => x.toLowerCase().match(/\p{Letter}+/gu) ?? []) - .flat(); - Promise.all(formatedTexts.map((t) => processSingleText(t, result))); - return result; -}; +const { + Worker, + isMainThread, + workerData, + parentPort, +} = require('node:worker_threads'); -const processSingleText = (text, result) => { - return new Promise((resolve) => { - let res = [...text].reduce((acc, cur) => { +if (isMainThread) { + module.exports.parallelLetterFrequency = async function (texts) { + let formatedTexts = texts + .map( + (x) => + x + .toLowerCase() + .match(/\p{Letter}+/gu) + ?.join('') ?? [], + ) + .flat(); + + return Promise.all(formatedTexts.map((t) => processSingleText(t))).then( + (results) => + results.reduce((acc, cur) => { + for (const letter in cur) { + acc[letter] = (acc[letter] || 0) + cur[letter]; + } + return acc; + }, {}), + ); + }; + + const processSingleText = (text) => { + return new Promise((resolve, reject) => { + const worker = new Worker(__filename, { + workerData: text, + }); + worker.on('message', resolve); + worker.on('error', reject); + }); + }; +} else { + const countInWorker = (data) => + [...data].reduce((acc, cur) => { acc[cur] = (acc[cur] || 0) + 1; return acc; - }, result); - resolve(res); - }); -}; + }, {}); + + parentPort.postMessage(countInWorker(workerData)); +} diff --git a/exercises/practice/parallel-letter-frequency/parallel-letter-frequency.js b/exercises/practice/parallel-letter-frequency/parallel-letter-frequency.js index 0396b88834..bef5eb5403 100644 --- a/exercises/practice/parallel-letter-frequency/parallel-letter-frequency.js +++ b/exercises/practice/parallel-letter-frequency/parallel-letter-frequency.js @@ -3,6 +3,6 @@ // convenience to get you started writing code faster. // -export const parallelLetterFrequency = (texts) => { +export const parallelLetterFrequency = async (texts) => { throw new Error('Remove this statement and implement this function'); }; diff --git a/exercises/practice/parallel-letter-frequency/parallel-letter-frequency.spec.js b/exercises/practice/parallel-letter-frequency/parallel-letter-frequency.spec.js index 3602e6af76..dfa1555c25 100644 --- a/exercises/practice/parallel-letter-frequency/parallel-letter-frequency.spec.js +++ b/exercises/practice/parallel-letter-frequency/parallel-letter-frequency.spec.js @@ -1,22 +1,22 @@ import { parallelLetterFrequency } from './parallel-letter-frequency'; describe('ParallelLetterFrequency', () => { - test('no texts', () => { + test('no texts', async () => { const expected = {}; const actual = parallelLetterFrequency([]); - expect(actual).toEqual(expected); + await expect(actual).resolves.toEqual(expected); }); - xtest('one text with one letter', () => { + xtest('one text with one letter', async () => { const texts = ['a']; const expected = { a: 1, }; const actual = parallelLetterFrequency(texts); - expect(actual).toEqual(expected); + await expect(actual).resolves.toEqual(expected); }); - xtest('one text with multiple letters', () => { + xtest('one text with multiple letters', async () => { const texts = ['bbcccd']; const expected = { b: 2, @@ -24,20 +24,20 @@ describe('ParallelLetterFrequency', () => { d: 1, }; const actual = parallelLetterFrequency(texts); - expect(actual).toEqual(expected); + await expect(actual).resolves.toEqual(expected); }); - xtest('two texts with one letter', () => { + xtest('two texts with one letter', async () => { const texts = ['e', 'f']; const expected = { e: 1, f: 1, }; const actual = parallelLetterFrequency(texts); - expect(actual).toEqual(expected); + await expect(actual).resolves.toEqual(expected); }); - xtest('two texts with multiple letters', () => { + xtest('two texts with multiple letters', async () => { const texts = ['ggh', 'hhi']; const expected = { g: 2, @@ -45,40 +45,40 @@ describe('ParallelLetterFrequency', () => { i: 1, }; const actual = parallelLetterFrequency(texts); - expect(actual).toEqual(expected); + await expect(actual).resolves.toEqual(expected); }); - xtest('ignore letter casing', () => { + xtest('ignore letter casing', async () => { const texts = ['m', 'M']; const expected = { m: 2, }; const actual = parallelLetterFrequency(texts); - expect(actual).toEqual(expected); + await expect(actual).resolves.toEqual(expected); }); - xtest('ignore whitespace', () => { + xtest('ignore whitespace', async () => { const texts = [' ', '\t', '\r\n']; const expected = {}; const actual = parallelLetterFrequency(texts); - expect(actual).toEqual(expected); + await expect(actual).resolves.toEqual(expected); }); - xtest('ignore punctuation', () => { + xtest('ignore punctuation', async () => { const texts = ['!', '?', ';', ',', '.']; const expected = {}; const actual = parallelLetterFrequency(texts); - expect(actual).toEqual(expected); + await expect(actual).resolves.toEqual(expected); }); - xtest('ignore numbers', () => { + xtest('ignore numbers', async () => { const texts = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0']; const expected = {}; const actual = parallelLetterFrequency(texts); - expect(actual).toEqual(expected); + await expect(actual).resolves.toEqual(expected); }); - xtest('Unicode letters', () => { + xtest('Unicode letters', async () => { const texts = ['本', 'φ', 'ほ', 'ø']; const expected = { 本: 1, @@ -87,10 +87,10 @@ describe('ParallelLetterFrequency', () => { ø: 1, }; const actual = parallelLetterFrequency(texts); - expect(actual).toEqual(expected); + await expect(actual).resolves.toEqual(expected); }); - xtest('combination of lower- and uppercase letters, punctuation and white space', () => { + xtest('combination of lower- and uppercase letters, punctuation and white space', async () => { const texts = [ 'There, peeping among the cloud-wrack above a dark tower high up in the mountains, Sam saw a white star twinkle for a while. The beauty of it smote his heart, as he looked up out of the forsaken land, and hope returned to him. For like a shaft, clear and cold, the thought pierced him that in the end, the shadow was only a small and passing thing: there was light and high beauty forever beyond its reach.', ]; @@ -119,10 +119,10 @@ describe('ParallelLetterFrequency', () => { y: 4, }; const actual = parallelLetterFrequency(texts); - expect(actual).toEqual(expected); + await expect(actual).resolves.toEqual(expected); }); - xtest('large texts', () => { + xtest('large texts', async () => { const texts = [ "I am a sick man.... I am a spiteful man. I am an unattractive man.\nI believe my liver is diseased. However, I know nothing at all about my disease, and do not\nknow for certain what ails me. I don't consult a doctor for it,\nand never have, though I have a respect for medicine and doctors.\nBesides, I am extremely superstitious, sufficiently so to respect medicine,\nanyway (I am well-educated enough not to be superstitious, but I am superstitious).\nNo, I refuse to consult a doctor from spite.\nThat you probably will not understand. Well, I understand it, though.\nOf course, I can't explain who it is precisely that I am mortifying in this case by my spite:\nI am perfectly well aware that I cannot \"pay out\" the doctors by not consulting them;\nI know better than anyone that by all this I am only injuring myself and no one else.\nBut still, if I don't consult a doctor it is from spite.\nMy liver is bad, well - let it get worse!\nI have been going on like that for a long time - twenty years. Now I am forty.\nI used to be in the government service, but am no longer.\nI was a spiteful official. I was rude and took pleasure in being so.\nI did not take bribes, you see, so I was bound to find a recompense in that, at least.\n(A poor jest, but I will not scratch it out. I wrote it thinking it would sound very witty;\nbut now that I have seen myself that I only wanted to show off in a despicable way -\nI will not scratch it out on purpose!) When petitioners used to come for\ninformation to the table at which I sat, I used to grind my teeth at them,\nand felt intense enjoyment when I succeeded in making anybody unhappy.\nI almost did succeed. For the most part they were all timid people - of course,\nthey were petitioners. But of the uppish ones there was one officer in particular\nI could not endure. He simply would not be humble, and clanked his sword in a disgusting way.\nI carried on a feud with him for eighteen months over that sword. At last I got the better of him.\nHe left off clanking it. That happened in my youth, though. But do you know,\ngentlemen, what was the chief point about my spite? Why, the whole point,\nthe real sting of it lay in the fact that continually, even in the moment of the acutest spleen,\nI was inwardly conscious with shame that I was not only not a spiteful but not even an embittered man,\nthat I was simply scaring sparrows at random and amusing myself by it.\nI might foam at the mouth, but bring me a doll to play with, give me a cup of tea with sugar in it,\nand maybe I should be appeased. I might even be genuinely touched,\nthough probably I should grind my teeth at myself afterwards and lie awake at night with shame for\nmonths after. That was my way. I was lying when I said just now that I was a spiteful official.\nI was lying from spite. I was simply amusing myself with the petitioners and with the officer,\nand in reality I never could become spiteful. I was conscious every moment in myself of many,\nvery many elements absolutely opposite to that. I felt them positively swarming in me,\nthese opposite elements. I knew that they had been swarming in me all my life and craving some outlet from me,\nbut I would not let them, would not let them, purposely would not let them come out.\nThey tormented me till I was ashamed: they drove me to convulsions and - sickened me, at last,\nhow they sickened me!", 'Gentlemen, I am joking, and I know myself that my jokes are not brilliant\n,but you know one can take everything as a joke. I am, perhaps, jesting against the grain.\nGentlemen, I am tormented by questions; answer them for me. You, for instance, want to cure men of their\nold habits and reform their will in accordance with science and good sense.\nBut how do you know, not only that it is possible, but also that it is\ndesirable to reform man in that way? And what leads you to the conclusion that man\'s\ninclinations need reforming? In short, how do you know that such a reformation will be a benefit to man?\nAnd to go to the root of the matter, why are you so positively convinced that not to act against\nhis real normal interests guaranteed by the conclusions of reason and arithmetic is certainly always\nadvantageous for man and must always be a law for mankind? So far, you know,\nthis is only your supposition. It may be the law of logic, but not the law of humanity.\nYou think, gentlemen, perhaps that I am mad? Allow me to defend myself. I agree that man\nis pre-eminently a creative animal, predestined to strive consciously for an object and to engage in engineering -\nthat is, incessantly and eternally to make new roads, wherever\nthey may lead. But the reason why he wants sometimes to go off at a tangent may just be that he is\npredestined to make the road, and perhaps, too, that however stupid the "direct"\npractical man may be, the thought sometimes will occur to him that the road almost always does lead\nsomewhere, and that the destination it leads to is less important than the process\nof making it, and that the chief thing is to save the well-conducted child from despising engineering,\nand so giving way to the fatal idleness, which, as we all know,\nis the mother of all the vices. Man likes to make roads and to create, that is a fact beyond dispute.\nBut why has he such a passionate love for destruction and chaos also?\nTell me that! But on that point I want to say a couple of words myself. May it not be that he loves\nchaos and destruction (there can be no disputing that he does sometimes love it)\nbecause he is instinctively afraid of attaining his object and completing the edifice he is constructing?\nWho knows, perhaps he only loves that edifice from a distance, and is by no means\nin love with it at close quarters; perhaps he only loves building it and does not want to live in it,\nbut will leave it, when completed, for the use of les animaux domestiques -\nsuch as the ants, the sheep, and so on. Now the ants have quite a different taste.\nThey have a marvellous edifice of that pattern which endures for ever - the ant-heap.\nWith the ant-heap the respectable race of ants began and with the ant-heap they will probably end,\nwhich does the greatest credit to their perseverance and good sense. But man is a frivolous and\nincongruous creature, and perhaps, like a chess player, loves the process of the game, not the end of it.\nAnd who knows (there is no saying with certainty), perhaps the only goal on earth\nto which mankind is striving lies in this incessant process of attaining, in other words,\nin life itself, and not in the thing to be attained, which must always be expressed as a formula,\nas positive as twice two makes four, and such positiveness is not life, gentlemen,\nbut is the beginning of death.', @@ -157,10 +157,10 @@ describe('ParallelLetterFrequency', () => { y: 251, }; const actual = parallelLetterFrequency(texts); - expect(actual).toEqual(expected); + await expect(actual).resolves.toEqual(expected); }); - xtest('many small texts', () => { + xtest('many small texts', async () => { const texts = Array(50).fill('abbccc'); const expected = { a: 50, @@ -168,6 +168,6 @@ describe('ParallelLetterFrequency', () => { c: 150, }; const actual = parallelLetterFrequency(texts); - expect(actual).toEqual(expected); + await expect(actual).resolves.toEqual(expected); }); }); From 6e1a2163512d2f89c564cdbd6bd61fe384dfec37 Mon Sep 17 00:00:00 2001 From: Cool-Katt Date: Sat, 23 Mar 2024 01:10:24 +0200 Subject: [PATCH 11/12] [New Exercise] Micro blog (#2402) * Adding autogenerated files * Added scaffolding and tests * Added working solution * Create instruction.append.md * Update config.json * Formatting * Adding approaches * Adding approaches * Fixing CI errors * Delete exercises/practice/micro-blog/.approaches/RegEx directory * Delete exercises/practice/micro-blog/.approaches/Iterators directory * Formatting, again * Explanations for edge-cases --- config.json | 10 +++ .../micro-blog/.approaches/config.json | 36 ++++++++ .../.approaches/intl-segmenter/content.md | 27 ++++++ .../.approaches/intl-segmenter/snippet.txt | 7 ++ .../micro-blog/.approaches/introduction.md | 64 ++++++++++++++ .../.approaches/iterators/content.md | 27 ++++++ .../.approaches/iterators/snippet.txt | 4 + .../micro-blog/.approaches/regex/content.md | 27 ++++++ .../micro-blog/.approaches/regex/snippet.txt | 4 + .../micro-blog/.docs/instruction.append.md | 33 +++++++ .../practice/micro-blog/.docs/instructions.md | 37 ++++++++ exercises/practice/micro-blog/.eslintrc | 14 +++ exercises/practice/micro-blog/.gitignore | 5 ++ .../practice/micro-blog/.meta/config.json | 17 ++++ .../practice/micro-blog/.meta/proof.ci.js | 4 + .../practice/micro-blog/.meta/tests.toml | 46 ++++++++++ exercises/practice/micro-blog/.npmrc | 1 + exercises/practice/micro-blog/LICENSE | 21 +++++ exercises/practice/micro-blog/babel.config.js | 4 + exercises/practice/micro-blog/micro-blog.js | 8 ++ .../practice/micro-blog/micro-blog.spec.js | 87 +++++++++++++++++++ exercises/practice/micro-blog/package.json | 34 ++++++++ 22 files changed, 517 insertions(+) create mode 100644 exercises/practice/micro-blog/.approaches/config.json create mode 100644 exercises/practice/micro-blog/.approaches/intl-segmenter/content.md create mode 100644 exercises/practice/micro-blog/.approaches/intl-segmenter/snippet.txt create mode 100644 exercises/practice/micro-blog/.approaches/introduction.md create mode 100644 exercises/practice/micro-blog/.approaches/iterators/content.md create mode 100644 exercises/practice/micro-blog/.approaches/iterators/snippet.txt create mode 100644 exercises/practice/micro-blog/.approaches/regex/content.md create mode 100644 exercises/practice/micro-blog/.approaches/regex/snippet.txt create mode 100644 exercises/practice/micro-blog/.docs/instruction.append.md create mode 100644 exercises/practice/micro-blog/.docs/instructions.md create mode 100644 exercises/practice/micro-blog/.eslintrc create mode 100644 exercises/practice/micro-blog/.gitignore create mode 100644 exercises/practice/micro-blog/.meta/config.json create mode 100644 exercises/practice/micro-blog/.meta/proof.ci.js create mode 100644 exercises/practice/micro-blog/.meta/tests.toml create mode 100644 exercises/practice/micro-blog/.npmrc create mode 100644 exercises/practice/micro-blog/LICENSE create mode 100644 exercises/practice/micro-blog/babel.config.js create mode 100644 exercises/practice/micro-blog/micro-blog.js create mode 100644 exercises/practice/micro-blog/micro-blog.spec.js create mode 100644 exercises/practice/micro-blog/package.json diff --git a/config.json b/config.json index 872ac1d1a8..64f0636092 100644 --- a/config.json +++ b/config.json @@ -2577,6 +2577,16 @@ "loops", "strings" ] + }, + { + "slug": "micro-blog", + "name": "Micro Blog", + "uuid": "ee771d09-33fb-4450-b9c3-d591a4a90a99", + "practices": [], + "prerequisites": [ + "strings" + ], + "difficulty": 2 } ] }, diff --git a/exercises/practice/micro-blog/.approaches/config.json b/exercises/practice/micro-blog/.approaches/config.json new file mode 100644 index 0000000000..48c2f79aa1 --- /dev/null +++ b/exercises/practice/micro-blog/.approaches/config.json @@ -0,0 +1,36 @@ +{ + "introduction": { + "authors": [ + "Cool-Katt" + ] + }, + "approaches": [ + { + "uuid": "ebd5893b-1f62-4634-a086-414338da1842", + "slug": "regex", + "title": "Regex", + "blurb": "Split a Unicode string using a RegEx.", + "authors": [ + "Cool-Katt" + ] + }, + { + "uuid": "a12fa836-201c-43bb-b7e2-28f441c270db", + "slug": "iterators", + "title": "Iterators", + "blurb": "Split a Unicode string using a string iterator.", + "authors": [ + "Cool-Katt" + ] + }, + { + "uuid": "c8b58d62-a129-41ad-afa6-cc6afb5b284c", + "slug": "intl-segmenter", + "title": "Intl.Segmenter", + "blurb": "Split a Unicode string using Intl.Segmenter.", + "authors": [ + "Cool-Katt" + ] + } + ] +} diff --git a/exercises/practice/micro-blog/.approaches/intl-segmenter/content.md b/exercises/practice/micro-blog/.approaches/intl-segmenter/content.md new file mode 100644 index 0000000000..26fcc70103 --- /dev/null +++ b/exercises/practice/micro-blog/.approaches/intl-segmenter/content.md @@ -0,0 +1,27 @@ +# Intl.Segmenter + +```javascript +let string = '👨‍👨‍👧‍👧💜🤧🤒🏥😀'; + +const splitWithSegmenter = (s) => + Array.from(new Intl.Segmenter().segment(String(s)), (x) => x.segment) + .slice(0, 5) + .join(''); + +console.log(splitWithSegmenter(string)); // will be "👨‍👨‍👧‍👧💜🤧🤒🏥" - correct, yay! +``` + +This solution: + +- Uses the [Intl.Segmenter object][segmenter] to split the string by graphemes and form an array from the result. +- Then it separates the first 5 graphemes. +- Finally, it joins them back into a string. + + +~~~~exercism/note +At the time of writing (February 2024) this method is not fully supported by the stable release of the Mozilla Firefox browser. +However, support for the Intl.Segmenter object is being worked on in the Nightly release of the browser. +~~~~ + + +[segmenter]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter diff --git a/exercises/practice/micro-blog/.approaches/intl-segmenter/snippet.txt b/exercises/practice/micro-blog/.approaches/intl-segmenter/snippet.txt new file mode 100644 index 0000000000..9778039865 --- /dev/null +++ b/exercises/practice/micro-blog/.approaches/intl-segmenter/snippet.txt @@ -0,0 +1,7 @@ +let string = '👨‍👨‍👧‍👧💜🤧🤒🏥😀'; + +const splitWithSegmenter = (s) => + Array.from(new Intl.Segmenter().segment(String(s)), (x) => x.segment) + .slice(0, 5) + .join(''); +console.log(splitWithSegmenter(string)); // will be "👨‍👨‍👧‍👧💜🤧🤒🏥" - correct, yay! \ No newline at end of file diff --git a/exercises/practice/micro-blog/.approaches/introduction.md b/exercises/practice/micro-blog/.approaches/introduction.md new file mode 100644 index 0000000000..fa943f5e43 --- /dev/null +++ b/exercises/practice/micro-blog/.approaches/introduction.md @@ -0,0 +1,64 @@ +# Introduction + +As noted in this exercise's introduction, most built-in Javascript methods for working with strings are Unicode-aware, but work with UTF-16 code units. +This might not be a problem, if all of the input contains characters represented by one code unit and you might not even notice it. +Unfortunately, this isn't the case with our micro-blog. + +Different approaches we'll compare include: + +- Using a `String iterator` +- Using a `Regular Expression` +- Using `Intl.Segmenter` + +## General guidance + +The main part of this exercise is figuring out how to split a Unicode encoded string and count up to 5 characters of it. + +## Approach: `String iterator` + +```javascript +function splitWithIterator(string) { + return [...string].slice(0, 5).join(''); +} +``` + +For more information, and a detailed explanation, check the [`String iterator` approach][iterator]. + +## Approach: `Regular Expression` + +```javascript +function splitWithRegex(string) { + return string.match(/.{0,5}/gu)[0]; +} +``` + +For more information, and a detailed explanation, check the [`Regular Expression` approach][regex] + +## Other approaches + +The aformentioned approaches both use UTF-16 code points, so character made of multiple code units aren't a problem. +But what about characters made of multiple code _points_, like some emoji? + +### Other approach: `Intl.Segmenter` + +The `Intl.Segmenter` object enables locale-sensitive string splitting and by default splits by graphemes, +so it should work well with symbols like emoji made of multiple code points. +For more information, and a detailed explanation, check the [`Intl.Segmenter` approach][separator]. + +## Which approach is the best in terms of performance? + +Testing with the following two strings on [JSBench.me][jsbench-me] yielded: + +```javascript +let string1 = '👨‍👨‍👧‍👧💜🤧🤒🏥😀'; +let string2 = 'The quick brown fox jumps over the lazy dog. It barked.'; +``` + +- The `String iterator` approach benched fastest. +- The `RegEx` approach was about 12% slower than the first. +- The `Intl.Segmenter` approach was the slowest of the three, by a considerable margin. + +[iterator]: https://exercism.org/tracks/javascript/exercises/micro-blog/approaches/iterators +[regex]: https://exercism.org/tracks/javascript/exercises/micro-blog/approaches/regex +[separator]: https://exercism.org/tracks/javascript/exercises/micro-blog/approaches/intl-segmenter +[jsbench-me]: https://jsbench.me/ diff --git a/exercises/practice/micro-blog/.approaches/iterators/content.md b/exercises/practice/micro-blog/.approaches/iterators/content.md new file mode 100644 index 0000000000..7f0a8e2a7c --- /dev/null +++ b/exercises/practice/micro-blog/.approaches/iterators/content.md @@ -0,0 +1,27 @@ +# Iterators + +```javascript +let string = '👨‍👨‍👧‍👧💜🤧🤒🏥😀'; +let string2 = 'The quick brown fox jumps over the lazy dog. It barked.'; + +const splitWithIterator = (s) => [...s].slice(0, 5).join(''); + +console.log(splitWithIterator(string)); // will be "👨‍👨‍👧" - incorrect +console.log(splitWithIterator(string2)); // will be "‍The q" +``` + +This solution: + +- Uses [spread syntax][spread] to unpack the string into an array of its characters. + - internaly, the spread operator works with iterators to separate the string by its code points. +- Then it separates the first 5 characters (code points). +- Finally, it joins them back into a string. + + +~~~~exercism/note +This approach will not yield the correct result when applied to characters that are made of multiple +graphere clusters and are meant to represent a single visual unit, such as some emoji. +~~~~ + + +[spread]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax diff --git a/exercises/practice/micro-blog/.approaches/iterators/snippet.txt b/exercises/practice/micro-blog/.approaches/iterators/snippet.txt new file mode 100644 index 0000000000..5bc8f56548 --- /dev/null +++ b/exercises/practice/micro-blog/.approaches/iterators/snippet.txt @@ -0,0 +1,4 @@ +let string = '👨‍👨‍👧‍👧💜🤧🤒🏥😀'; + +const splitWithIterator = (s) => [...s].slice(0, 5).join(''); +console.log(splitWithIterator(string)) // will be "👨‍👨‍👧" - incorrect \ No newline at end of file diff --git a/exercises/practice/micro-blog/.approaches/regex/content.md b/exercises/practice/micro-blog/.approaches/regex/content.md new file mode 100644 index 0000000000..70b7216568 --- /dev/null +++ b/exercises/practice/micro-blog/.approaches/regex/content.md @@ -0,0 +1,27 @@ +# Regex + +```javascript +let string = '👨‍👨‍👧‍👧💜🤧🤒🏥😀'; +let string2 = 'The quick brown fox jumps over the lazy dog. It barked.'; + +const splitWithRegEx = (s) => s.match(/.{0,5}/gu)[0]; + +console.log(splitWithRegEx(string)); // will be "👨‍👨‍👧" - incorrect +console.log(splitWithIterator(string2)); // will be "‍The q" +``` + +This solution: + +- Uses the [String.match() method][match] with a supplied RegEx + - The RegEx supplied matches any character `.`, between 0 and 5 times `{0, 5}`. The `u` flag enables Unicode support. + - This matches characters by code points as well. +- Then it returns the first match as the output string. + + +~~~~exercism/note +This approach will not yield the correct result when applied to characters that are made of multiple +graphere clusters and are meant to represent a single visual unit, such as some emoji. +~~~~ + + +[match]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match diff --git a/exercises/practice/micro-blog/.approaches/regex/snippet.txt b/exercises/practice/micro-blog/.approaches/regex/snippet.txt new file mode 100644 index 0000000000..48961397da --- /dev/null +++ b/exercises/practice/micro-blog/.approaches/regex/snippet.txt @@ -0,0 +1,4 @@ +let string = '👨‍👨‍👧‍👧💜🤧🤒🏥😀'; + +const splitWithRegEx = (s) => s.match(/.{0,5}/gu)[0]; +console.log(splitWithRegEx(string)); // will be "👨‍👨‍👧" - incorrect \ No newline at end of file diff --git a/exercises/practice/micro-blog/.docs/instruction.append.md b/exercises/practice/micro-blog/.docs/instruction.append.md new file mode 100644 index 0000000000..cb285c5758 --- /dev/null +++ b/exercises/practice/micro-blog/.docs/instruction.append.md @@ -0,0 +1,33 @@ +# Instruction append + +## Unicode code points vs code units. + +A "normal" UTF-16 encoded string can be represented as a series of characters, where each character can be up to 16 bits long (hence, the name UTF-16). +This means there are a maximum of 2¹⁶ (two to the power of sixteen), or 65536 possible characters representable with 16 bits, or 1 code **unit**. +These 65536 characters form what's known as the [Basic Multilingual Set][basic-multilingual-set], which is large enough for the most common characters of most languages. + +However, some symbols, can't fit in just 1 code unit. The solution is to represent them with two code units. +These two UTF-16 code units, often also reffered to as a _surrogate pair_, form a code **point**. + +So, in summary, when reffering to UTF-16 encoding: + +- A `code unit` is 16 (or less) bits representing a single character. +- A `code point` is one or two code units representing a single character. + +To add more confusion to the mix, theres also _grapheme clusters_, +that are basically sequences of Unicode characters (code points) that should be treated as a single visual unit. +For example, some emojis, like this one 👨‍👦. + +## UTF-16 in Javascript + +Most built-in Javascript methods will work with UTF-16 encoded strings, however they work based on UTF-16 code units. +For example, a [`String.prototype.split("")`][split] method will separate a string by code units. + +On the other hand, [`String iterators`][iterator] iterate by code points. + +You can read a lot more, and find examples about Unicode strings, on [MDN][MDN]. + +[basic-multilingual-set]: https://en.wikipedia.org/wiki/Plane_(Unicode)#Basic_Multilingual_Plane +[split]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split +[iterator]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/@@iterator +[MDN]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#utf-16_characters_unicode_code_points_and_grapheme_clusters diff --git a/exercises/practice/micro-blog/.docs/instructions.md b/exercises/practice/micro-blog/.docs/instructions.md new file mode 100644 index 0000000000..d6c6cf6561 --- /dev/null +++ b/exercises/practice/micro-blog/.docs/instructions.md @@ -0,0 +1,37 @@ +# Instructions + +You have identified a gap in the social media market for very very short posts. +Now that Twitter allows 280 character posts, people wanting quick social media updates aren't being served. +You decide to create your own social media network. + +To make your product noteworthy, you make it extreme and only allow posts of 5 or less characters. +Any posts of more than 5 characters should be truncated to 5. + +To allow your users to express themselves fully, you allow Emoji and other Unicode. + +The task is to truncate input strings to 5 characters. + +## Text Encodings + +Text stored digitally has to be converted to a series of bytes. +There are 3 ways to map characters to bytes in common use. + +- **ASCII** can encode English language characters. + All characters are precisely 1 byte long. +- **UTF-8** is a Unicode text encoding. + Characters take between 1 and 4 bytes. +- **UTF-16** is a Unicode text encoding. + Characters are either 2 or 4 bytes long. + +UTF-8 and UTF-16 are both Unicode encodings which means they're capable of representing a massive range of characters including: + +- Text in most of the world's languages and scripts +- Historic text +- Emoji + +UTF-8 and UTF-16 are both variable length encodings, which means that different characters take up different amounts of space. + +Consider the letter 'a' and the emoji '😛'. +In UTF-16 the letter takes 2 bytes but the emoji takes 4 bytes. + +The trick to this exercise is to use APIs designed around Unicode characters (codepoints) instead of Unicode codeunits. diff --git a/exercises/practice/micro-blog/.eslintrc b/exercises/practice/micro-blog/.eslintrc new file mode 100644 index 0000000000..1d4446029c --- /dev/null +++ b/exercises/practice/micro-blog/.eslintrc @@ -0,0 +1,14 @@ +{ + "root": true, + "extends": "@exercism/eslint-config-javascript", + "env": { + "jest": true + }, + "overrides": [ + { + "files": [".meta/proof.ci.js", ".meta/exemplar.js", "*.spec.js"], + "excludedFiles": ["custom.spec.js"], + "extends": "@exercism/eslint-config-javascript/maintainers" + } + ] +} diff --git a/exercises/practice/micro-blog/.gitignore b/exercises/practice/micro-blog/.gitignore new file mode 100644 index 0000000000..31c57dd53a --- /dev/null +++ b/exercises/practice/micro-blog/.gitignore @@ -0,0 +1,5 @@ +/node_modules +/bin/configlet +/bin/configlet.exe +/pnpm-lock.yaml +/yarn.lock diff --git a/exercises/practice/micro-blog/.meta/config.json b/exercises/practice/micro-blog/.meta/config.json new file mode 100644 index 0000000000..e33da00673 --- /dev/null +++ b/exercises/practice/micro-blog/.meta/config.json @@ -0,0 +1,17 @@ +{ + "authors": [ + "Cool-Katt" + ], + "files": { + "solution": [ + "micro-blog.js" + ], + "test": [ + "micro-blog.spec.js" + ], + "example": [ + ".meta/proof.ci.js" + ] + }, + "blurb": "Given an input string, truncate it to 5 characters." +} diff --git a/exercises/practice/micro-blog/.meta/proof.ci.js b/exercises/practice/micro-blog/.meta/proof.ci.js new file mode 100644 index 0000000000..943829e3d4 --- /dev/null +++ b/exercises/practice/micro-blog/.meta/proof.ci.js @@ -0,0 +1,4 @@ +export const truncate = (input) => + Array.from(new Intl.Segmenter().segment(String(input)), (x) => x.segment) + .slice(0, 5) + .join(''); diff --git a/exercises/practice/micro-blog/.meta/tests.toml b/exercises/practice/micro-blog/.meta/tests.toml new file mode 100644 index 0000000000..f23ff0bc22 --- /dev/null +++ b/exercises/practice/micro-blog/.meta/tests.toml @@ -0,0 +1,46 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[b927b57f-7c98-42fd-8f33-fae091dc1efc] +description = "English language short" + +[a3fcdc5b-0ed4-4f49-80f5-b1a293eac2a0] +description = "English language long" + +[01910864-8e15-4007-9c7c-ac956c686e60] +description = "German language short (broth)" + +[f263e488-aefb-478f-a671-b6ba99722543] +description = "German language long (bear carpet → beards)" + +[0916e8f1-41d7-4402-a110-b08aa000342c] +description = "Bulgarian language short (good)" + +[bed6b89c-03df-4154-98e6-a61a74f61b7d] +description = "Greek language short (health)" + +[485a6a70-2edb-424d-b999-5529dbc8e002] +description = "Maths short" + +[8b4b7b51-8f48-4fbe-964e-6e4e6438be28] +description = "Maths long" + +[71f4a192-0566-4402-a512-fe12878be523] +description = "English and emoji short" + +[6f0f71f3-9806-4759-a844-fa182f7bc203] +description = "Emoji short" + +[ce71fb92-5214-46d0-a7f8-d5ba56b4cc6e] +description = "Emoji long" + +[5dee98d2-d56e-468a-a1f2-121c3f7c5a0b] +description = "Royal Flush?" diff --git a/exercises/practice/micro-blog/.npmrc b/exercises/practice/micro-blog/.npmrc new file mode 100644 index 0000000000..d26df800bb --- /dev/null +++ b/exercises/practice/micro-blog/.npmrc @@ -0,0 +1 @@ +audit=false diff --git a/exercises/practice/micro-blog/LICENSE b/exercises/practice/micro-blog/LICENSE new file mode 100644 index 0000000000..90e73be03b --- /dev/null +++ b/exercises/practice/micro-blog/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Exercism + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/exercises/practice/micro-blog/babel.config.js b/exercises/practice/micro-blog/babel.config.js new file mode 100644 index 0000000000..b781d5a667 --- /dev/null +++ b/exercises/practice/micro-blog/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: ['@exercism/babel-preset-javascript'], + plugins: [], +}; diff --git a/exercises/practice/micro-blog/micro-blog.js b/exercises/practice/micro-blog/micro-blog.js new file mode 100644 index 0000000000..4ebe04e779 --- /dev/null +++ b/exercises/practice/micro-blog/micro-blog.js @@ -0,0 +1,8 @@ +// +// This is only a SKELETON file for the 'Micro-blog' exercise. It's been provided as a +// convenience to get you started writing code faster. +// + +export const truncate = (input) => { + throw new Error('Remove this statement and implement this function'); +}; diff --git a/exercises/practice/micro-blog/micro-blog.spec.js b/exercises/practice/micro-blog/micro-blog.spec.js new file mode 100644 index 0000000000..62972fbbc8 --- /dev/null +++ b/exercises/practice/micro-blog/micro-blog.spec.js @@ -0,0 +1,87 @@ +import { truncate } from './micro-blog'; + +describe('Micro-blog', () => { + test('English language short', () => { + const inputString = 'Hi'; + const expected = 'Hi'; + const actual = truncate(inputString); + expect(actual).toEqual(expected); + }); + + xtest('English language long', () => { + const inputString = 'Hello there'; + const expected = 'Hello'; + const actual = truncate(inputString); + expect(actual).toEqual(expected); + }); + + xtest('German language short (broth)', () => { + const inputString = 'brühe'; + const expected = 'brühe'; + const actual = truncate(inputString); + expect(actual).toEqual(expected); + }); + + xtest('German language long (bear carpet → beards)', () => { + const inputString = 'Bärteppich'; + const expected = 'Bärte'; + const actual = truncate(inputString); + expect(actual).toEqual(expected); + }); + + xtest('Bulgarian language short (good)', () => { + const inputString = 'Добър'; + const expected = 'Добър'; + const actual = truncate(inputString); + expect(actual).toEqual(expected); + }); + + xtest('Greek language short (health)', () => { + const inputString = 'υγειά'; + const expected = 'υγειά'; + const actual = truncate(inputString); + expect(actual).toEqual(expected); + }); + + xtest('Maths short', () => { + const inputString = 'a=πr²'; + const expected = 'a=πr²'; + const actual = truncate(inputString); + expect(actual).toEqual(expected); + }); + + xtest('Maths long', () => { + const inputString = '∅⊊ℕ⊊ℤ⊊ℚ⊊ℝ⊊ℂ'; + const expected = '∅⊊ℕ⊊ℤ'; + const actual = truncate(inputString); + expect(actual).toEqual(expected); + }); + + xtest('English and emoji short', () => { + const inputString = 'Fly 🛫'; + const expected = 'Fly 🛫'; + const actual = truncate(inputString); + expect(actual).toEqual(expected); + }); + + xtest('Emoji short', () => { + const inputString = '💇'; + const expected = '💇'; + const actual = truncate(inputString); + expect(actual).toEqual(expected); + }); + + xtest('Emoji long', () => { + const inputString = '❄🌡🤧🤒🏥🕰😀'; + const expected = '❄🌡🤧🤒🏥'; + const actual = truncate(inputString); + expect(actual).toEqual(expected); + }); + + xtest('Royal Flush?', () => { + const inputString = '🃎🂸🃅🃋🃍🃁🃊'; + const expected = '🃎🂸🃅🃋🃍'; + const actual = truncate(inputString); + expect(actual).toEqual(expected); + }); +}); diff --git a/exercises/practice/micro-blog/package.json b/exercises/practice/micro-blog/package.json new file mode 100644 index 0000000000..851420556a --- /dev/null +++ b/exercises/practice/micro-blog/package.json @@ -0,0 +1,34 @@ +{ + "name": "@exercism/javascript-micro-blog", + "description": "Exercism practice exercise on micro-blog", + "author": "Katrina Owen", + "contributors": [ + "Cool-Katt (https://github.com/Cool-Katt)", + "Derk-Jan Karrenbeld (https://derk-jan.com)", + "Tejas Bubane (https://tejasbubane.github.io/)" + ], + "private": true, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/exercism/javascript", + "directory": "exercises/practice/micro-blog" + }, + "devDependencies": { + "@babel/core": "^7.23.0", + "@exercism/babel-preset-javascript": "^0.2.1", + "@exercism/eslint-config-javascript": "^0.6.0", + "@types/jest": "^29.5.4", + "@types/node": "^20.5.6", + "babel-jest": "^29.6.4", + "core-js": "~3.32.2", + "eslint": "^8.49.0", + "jest": "^29.7.0" + }, + "dependencies": {}, + "scripts": { + "test": "jest ./*", + "watch": "jest --watch ./*", + "lint": "eslint ." + } +} From 2447138427c16694f952a4b8ede4de9a75e0fd6c Mon Sep 17 00:00:00 2001 From: Cool-Katt Date: Sat, 23 Mar 2024 10:35:04 +0200 Subject: [PATCH 12/12] [New exercise] Markdown (#2401) * Adding autogenerated files * Adding tests * Added code to be refactored * Update markdown.spec.js * Added a working solution * Formatting --- config.json | 11 ++ .../practice/markdown/.docs/instructions.md | 13 ++ exercises/practice/markdown/.eslintrc | 14 ++ exercises/practice/markdown/.gitignore | 5 + exercises/practice/markdown/.meta/config.json | 17 +++ exercises/practice/markdown/.meta/proof.ci.js | 55 ++++++++ exercises/practice/markdown/.meta/tests.toml | 66 +++++++++ exercises/practice/markdown/.npmrc | 1 + exercises/practice/markdown/LICENSE | 21 +++ exercises/practice/markdown/babel.config.js | 4 + exercises/practice/markdown/markdown.js | 101 ++++++++++++++ exercises/practice/markdown/markdown.spec.js | 127 ++++++++++++++++++ exercises/practice/markdown/package.json | 34 +++++ 13 files changed, 469 insertions(+) create mode 100644 exercises/practice/markdown/.docs/instructions.md create mode 100644 exercises/practice/markdown/.eslintrc create mode 100644 exercises/practice/markdown/.gitignore create mode 100644 exercises/practice/markdown/.meta/config.json create mode 100644 exercises/practice/markdown/.meta/proof.ci.js create mode 100644 exercises/practice/markdown/.meta/tests.toml create mode 100644 exercises/practice/markdown/.npmrc create mode 100644 exercises/practice/markdown/LICENSE create mode 100644 exercises/practice/markdown/babel.config.js create mode 100644 exercises/practice/markdown/markdown.js create mode 100644 exercises/practice/markdown/markdown.spec.js create mode 100644 exercises/practice/markdown/package.json diff --git a/config.json b/config.json index 64f0636092..d4b1d7a260 100644 --- a/config.json +++ b/config.json @@ -2578,6 +2578,17 @@ "strings" ] }, + { + "slug": "markdown", + "name": "Markdown", + "uuid": "cd666b3a-7114-4ba9-9b2a-7622a2c8c12c", + "practices": [ + "strings", + "string-formatting" + ], + "prerequisites": [], + "difficulty": 5 + }, { "slug": "micro-blog", "name": "Micro Blog", diff --git a/exercises/practice/markdown/.docs/instructions.md b/exercises/practice/markdown/.docs/instructions.md new file mode 100644 index 0000000000..9b756d9917 --- /dev/null +++ b/exercises/practice/markdown/.docs/instructions.md @@ -0,0 +1,13 @@ +# Instructions + +Refactor a Markdown parser. + +The markdown exercise is a refactoring exercise. +There is code that parses a given string with [Markdown syntax][markdown] and returns the associated HTML for that string. +Even though this code is confusingly written and hard to follow, somehow it works and all the tests are passing! +Your challenge is to re-write this code to make it easier to read and maintain while still making sure that all the tests keep passing. + +It would be helpful if you made notes of what you did in your refactoring in comments so reviewers can see that, but it isn't strictly necessary. +The most important thing is to make the code better! + +[markdown]: https://guides.github.com/features/mastering-markdown/ diff --git a/exercises/practice/markdown/.eslintrc b/exercises/practice/markdown/.eslintrc new file mode 100644 index 0000000000..1d4446029c --- /dev/null +++ b/exercises/practice/markdown/.eslintrc @@ -0,0 +1,14 @@ +{ + "root": true, + "extends": "@exercism/eslint-config-javascript", + "env": { + "jest": true + }, + "overrides": [ + { + "files": [".meta/proof.ci.js", ".meta/exemplar.js", "*.spec.js"], + "excludedFiles": ["custom.spec.js"], + "extends": "@exercism/eslint-config-javascript/maintainers" + } + ] +} diff --git a/exercises/practice/markdown/.gitignore b/exercises/practice/markdown/.gitignore new file mode 100644 index 0000000000..31c57dd53a --- /dev/null +++ b/exercises/practice/markdown/.gitignore @@ -0,0 +1,5 @@ +/node_modules +/bin/configlet +/bin/configlet.exe +/pnpm-lock.yaml +/yarn.lock diff --git a/exercises/practice/markdown/.meta/config.json b/exercises/practice/markdown/.meta/config.json new file mode 100644 index 0000000000..63d94efeb4 --- /dev/null +++ b/exercises/practice/markdown/.meta/config.json @@ -0,0 +1,17 @@ +{ + "authors": [ + "Cool-Katt" + ], + "files": { + "solution": [ + "markdown.js" + ], + "test": [ + "markdown.spec.js" + ], + "example": [ + ".meta/proof.ci.js" + ] + }, + "blurb": "Refactor a Markdown parser." +} diff --git a/exercises/practice/markdown/.meta/proof.ci.js b/exercises/practice/markdown/.meta/proof.ci.js new file mode 100644 index 0000000000..6d7ccde979 --- /dev/null +++ b/exercises/practice/markdown/.meta/proof.ci.js @@ -0,0 +1,55 @@ +const wrapInTag = (tag, text) => `<${tag}>${text}`; + +const REPLACERS = { + paragraph: { + operationNumber: 1, + regexPattern: /^(.+)$/gim, + replacer: (match, p1) => + match.startsWith('*') || match.startsWith('#') + ? match + : wrapInTag('p', p1), + }, + heading: { + operationNumber: 2, + regexPattern: /^(#{1,7})\s*(.+)/gim, + replacer: (match, p1, p2) => + p1.length > 6 ? wrapInTag('p', match) : wrapInTag(`h${p1.length}`, p2), + }, + bold: { + operationNumber: 3, + regexPattern: /__(.+)__/gim, + replacer: (match, p1) => wrapInTag('strong', p1), + }, + italic: { + operationNumber: 4, + regexPattern: /_(.+)_/gim, + replacer: (match, p1) => wrapInTag('em', p1), + }, + listItem: { + operationNumber: 5, + regexPattern: /^\*\s(.+)/gim, + replacer: (match, p1) => wrapInTag('li', p1), + }, + newLine: { + operationNumber: 6, + regexPattern: /\n/gim, + replacer: () => '', + }, + list: { + operationNumber: 7, + regexPattern: /(
  • .+<\/li>)/gim, + replacer: (match, p1) => wrapInTag('ul', p1), + }, +}; + +const sortedOperations = Object.values(REPLACERS).sort( + (a, b) => a.operationNumber - b.operationNumber, +); + +export function parse(markdown) { + return sortedOperations.reduce( + (text, { regexPattern, replacer }) => + text.replaceAll(new RegExp(regexPattern), replacer), + markdown, + ); +} diff --git a/exercises/practice/markdown/.meta/tests.toml b/exercises/practice/markdown/.meta/tests.toml new file mode 100644 index 0000000000..28b7baa720 --- /dev/null +++ b/exercises/practice/markdown/.meta/tests.toml @@ -0,0 +1,66 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[e75c8103-a6b8-45d9-84ad-e68520545f6e] +description = "parses normal text as a paragraph" + +[69a4165d-9bf8-4dd7-bfdc-536eaca80a6a] +description = "parsing italics" + +[ec345a1d-db20-4569-a81a-172fe0cad8a1] +description = "parsing bold text" + +[51164ed4-5641-4909-8fab-fbaa9d37d5a8] +description = "mixed normal, italics and bold text" + +[ad85f60d-0edd-4c6a-a9b1-73e1c4790d15] +description = "with h1 header level" + +[d0f7a31f-6935-44ac-8a9a-1e8ab16af77f] +description = "with h2 header level" + +[9df3f500-0622-4696-81a7-d5babd9b5f49] +description = "with h3 header level" + +[50862777-a5e8-42e9-a3b8-4ba6fcd0ed03] +description = "with h4 header level" + +[ee1c23ac-4c86-4f2a-8b9c-403548d4ab82] +description = "with h5 header level" + +[13b5f410-33f5-44f0-a6a7-cfd4ab74b5d5] +description = "with h6 header level" + +[6dca5d10-5c22-4e2a-ac2b-bd6f21e61939] +description = "with h7 header level" +include = false + +[81c0c4db-435e-4d77-860d-45afacdad810] +description = "h7 header level is a paragraph" +reimplements = "6dca5d10-5c22-4e2a-ac2b-bd6f21e61939" + +[25288a2b-8edc-45db-84cf-0b6c6ee034d6] +description = "unordered lists" + +[7bf92413-df8f-4de8-9184-b724f363c3da] +description = "With a little bit of everything" + +[0b3ed1ec-3991-4b8b-8518-5cb73d4a64fe] +description = "with markdown symbols in the header text that should not be interpreted" + +[113a2e58-78de-4efa-90e9-20972224d759] +description = "with markdown symbols in the list item text that should not be interpreted" + +[e65e46e2-17b7-4216-b3ac-f44a1b9bcdb4] +description = "with markdown symbols in the paragraph text that should not be interpreted" + +[f0bbbbde-0f52-4c0c-99ec-be4c60126dd4] +description = "unordered lists close properly with preceding and following lines" diff --git a/exercises/practice/markdown/.npmrc b/exercises/practice/markdown/.npmrc new file mode 100644 index 0000000000..d26df800bb --- /dev/null +++ b/exercises/practice/markdown/.npmrc @@ -0,0 +1 @@ +audit=false diff --git a/exercises/practice/markdown/LICENSE b/exercises/practice/markdown/LICENSE new file mode 100644 index 0000000000..90e73be03b --- /dev/null +++ b/exercises/practice/markdown/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Exercism + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/exercises/practice/markdown/babel.config.js b/exercises/practice/markdown/babel.config.js new file mode 100644 index 0000000000..b781d5a667 --- /dev/null +++ b/exercises/practice/markdown/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: ['@exercism/babel-preset-javascript'], + plugins: [], +}; diff --git a/exercises/practice/markdown/markdown.js b/exercises/practice/markdown/markdown.js new file mode 100644 index 0000000000..23b2e82092 --- /dev/null +++ b/exercises/practice/markdown/markdown.js @@ -0,0 +1,101 @@ +function wrap(text, tag) { + return `<${tag}>${text}`; +} + +function isTag(text, tag) { + return text.startsWith(`<${tag}>`); +} + +function parser(markdown, delimiter, tag) { + const pattern = new RegExp(`${delimiter}(.+)${delimiter}`); + const replacement = `<${tag}>$1`; + return markdown.replace(pattern, replacement); +} + +function parse__(markdown) { + return parser(markdown, '__', 'strong'); +} + +function parse_(markdown) { + return parser(markdown, '_', 'em'); +} + +function parseText(markdown, list) { + const parsedText = parse_(parse__(markdown)); + if (list) { + return parsedText; + } else { + return wrap(parsedText, 'p'); + } +} + +function parseHeader(markdown, list) { + let count = 0; + for (let i = 0; i < markdown.length; i++) { + if (markdown[i] === '#') { + count += 1; + } else { + break; + } + } + if (count === 0 || count > 6) { + return [null, list]; + } + const headerTag = `h${count}`; + const headerHtml = wrap(markdown.substring(count + 1), headerTag); + if (list) { + return [`${headerHtml}`, false]; + } else { + return [headerHtml, false]; + } +} + +function parseLineItem(markdown, list) { + if (markdown.startsWith('*')) { + const innerHtml = wrap(parseText(markdown.substring(2), true), 'li'); + if (list) { + return [innerHtml, true]; + } else { + return [`
      ${innerHtml}`, true]; + } + } + return [null, list]; +} + +function parseParagraph(markdown, list) { + if (!list) { + return [parseText(markdown, false), false]; + } else { + return [`
    ${parseText(markdown, false)}`, false]; + } +} + +function parseLine(markdown, list) { + let [result, inListAfter] = parseHeader(markdown, list); + if (result === null) { + [result, inListAfter] = parseLineItem(markdown, list); + } + if (result === null) { + [result, inListAfter] = parseParagraph(markdown, list); + } + if (result === null) { + throw new Error('Invalid markdown'); + } + return [result, inListAfter]; +} + +export function parse(markdown) { + const lines = markdown.split('\n'); + let result = ''; + let list = false; + for (let i = 0; i < lines.length; i++) { + let [lineResult, newList] = parseLine(lines[i], list); + result += lineResult; + list = newList; + } + if (list) { + return result + ''; + } else { + return result; + } +} diff --git a/exercises/practice/markdown/markdown.spec.js b/exercises/practice/markdown/markdown.spec.js new file mode 100644 index 0000000000..db3fea3ff6 --- /dev/null +++ b/exercises/practice/markdown/markdown.spec.js @@ -0,0 +1,127 @@ +import { parse } from './markdown'; + +describe('Markdown', () => { + test('parses normal text as a paragraph', () => { + const markdown = 'This will be a paragraph'; + const expected = '

    This will be a paragraph

    '; + const actual = parse(markdown); + expect(actual).toEqual(expected); + }); + + xtest('parsing italics', () => { + const markdown = '_This will be italic_'; + const expected = '

    This will be italic

    '; + const actual = parse(markdown); + expect(actual).toEqual(expected); + }); + + xtest('parsing bold text', () => { + const markdown = '__This will be bold__'; + const expected = '

    This will be bold

    '; + const actual = parse(markdown); + expect(actual).toEqual(expected); + }); + + xtest('mixed normal, italics and bold text', () => { + const markdown = 'This will _be_ __mixed__'; + const expected = '

    This will be mixed

    '; + const actual = parse(markdown); + expect(actual).toEqual(expected); + }); + + xtest('with h1 header level', () => { + const markdown = '# This will be an h1'; + const expected = '

    This will be an h1

    '; + const actual = parse(markdown); + expect(actual).toEqual(expected); + }); + + xtest('with h2 header level', () => { + const markdown = '## This will be an h2'; + const expected = '

    This will be an h2

    '; + const actual = parse(markdown); + expect(actual).toEqual(expected); + }); + + xtest('with h3 header level', () => { + const markdown = '### This will be an h3'; + const expected = '

    This will be an h3

    '; + const actual = parse(markdown); + expect(actual).toEqual(expected); + }); + + xtest('with h4 header level', () => { + const markdown = '#### This will be an h4'; + const expected = '

    This will be an h4

    '; + const actual = parse(markdown); + expect(actual).toEqual(expected); + }); + + xtest('with h5 header level', () => { + const markdown = '##### This will be an h5'; + const expected = '
    This will be an h5
    '; + const actual = parse(markdown); + expect(actual).toEqual(expected); + }); + + xtest('with h6 header level', () => { + const markdown = '###### This will be an h6'; + const expected = '
    This will be an h6
    '; + const actual = parse(markdown); + expect(actual).toEqual(expected); + }); + + xtest('with h7 header level', () => { + const markdown = '####### This will not be an h7'; + const expected = '

    ####### This will not be an h7

    '; + const actual = parse(markdown); + expect(actual).toEqual(expected); + }); + + xtest('unordered lists', () => { + const markdown = '* Item 1\n' + '* Item 2'; + const expected = '
    • Item 1
    • Item 2
    '; + const actual = parse(markdown); + expect(actual).toEqual(expected); + }); + + xtest('with a little bit of everything', () => { + const markdown = '# Header!\n' + '* __Bold Item__\n' + '* _Italic Item_'; + const expected = + '

    Header!

    • Bold Item
    • Italic Item
    '; + const actual = parse(markdown); + expect(actual).toEqual(expected); + }); + + xtest('with markdown symbols in the header text that should not be interpreted', () => { + const markdown = '# This is a header with # and * in the text'; + const expected = '

    This is a header with # and * in the text

    '; + const actual = parse(markdown); + expect(actual).toEqual(expected); + }); + + xtest('with markdown symbols in the list item text that should not be interpreted', () => { + const markdown = + '* Item 1 with a # in the text\n' + '* Item 2 with * in the text'; + const expected = + '
    • Item 1 with a # in the text
    • Item 2 with * in the text
    '; + const actual = parse(markdown); + expect(actual).toEqual(expected); + }); + + xtest('with markdown symbols in the paragraph text that should not be interpreted', () => { + const markdown = 'This is a paragraph with # and * in the text'; + const expected = '

    This is a paragraph with # and * in the text

    '; + const actual = parse(markdown); + expect(actual).toEqual(expected); + }); + + xtest('unordered lists close properly with preceding and following lines', () => { + const markdown = + '# Start a list\n' + '* Item 1\n' + '* Item 2\n' + 'End a list'; + const expected = + '

    Start a list

    • Item 1
    • Item 2

    End a list

    '; + const actual = parse(markdown); + expect(actual).toEqual(expected); + }); +}); diff --git a/exercises/practice/markdown/package.json b/exercises/practice/markdown/package.json new file mode 100644 index 0000000000..3da317b27c --- /dev/null +++ b/exercises/practice/markdown/package.json @@ -0,0 +1,34 @@ +{ + "name": "@exercism/javascript-markdown", + "description": "Exercism practice exercise on markdown", + "author": "Katrina Owen", + "contributors": [ + "Cool-Katt (https://github.com/Cool-Katt)", + "Derk-Jan Karrenbeld (https://derk-jan.com)", + "Tejas Bubane (https://tejasbubane.github.io/)" + ], + "private": true, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/exercism/javascript", + "directory": "exercises/practice/markdown" + }, + "devDependencies": { + "@babel/core": "^7.23.0", + "@exercism/babel-preset-javascript": "^0.2.1", + "@exercism/eslint-config-javascript": "^0.6.0", + "@types/jest": "^29.5.4", + "@types/node": "^20.5.6", + "babel-jest": "^29.6.4", + "core-js": "~3.32.2", + "eslint": "^8.49.0", + "jest": "^29.7.0" + }, + "dependencies": {}, + "scripts": { + "test": "jest ./*", + "watch": "jest --watch ./*", + "lint": "eslint ." + } +}