diff --git a/.eslintignore b/.eslintignore
deleted file mode 100644
index 06be59d510..0000000000
--- a/.eslintignore
+++ /dev/null
@@ -1,16 +0,0 @@
-!.meta
-
-# Protected or generated
-/.appends
-/.github
-/.vscode
-
-# Binaries
-/bin
-
-# Configuration
-/config
-
-# Typings
-/exercises/**/global.d.ts
-/exercises/**/env.d.ts
diff --git a/.eslintrc b/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/.github/dependabot.yml b/.github/dependabot.yml
index 16287ebc2c..b0c1effe76 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -5,7 +5,7 @@ updates:
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
- interval: 'daily'
+ interval: 'monthly'
labels:
- 'dependencies :arrow_up_small:'
- 'github_actions'
@@ -17,7 +17,7 @@ updates:
directory: '/'
# Check the npm registry for updates every day (weekdays)
schedule:
- interval: 'daily'
+ interval: 'monthly'
labels:
- 'dependencies :arrow_up_small:'
- 'javascript'
diff --git a/.github/labels.yml b/.github/labels.yml
index 72e46ba37f..775388e833 100644
--- a/.github/labels.yml
+++ b/.github/labels.yml
@@ -157,16 +157,16 @@
description: "Work on Documentation"
color: "ffffff"
-# This label can be added to accept PRs as part of Hacktoberfest
-- name: "hacktoberfest-accepted"
- description: "Make this PR count for hacktoberfest"
- color: "ff7518"
-
# This Exercism-wide label is added to all automatically created pull requests that help migrate/prepare a track for Exercism v3
- name: "v3-migration 🤖"
description: "Preparing for Exercism v3"
color: "e99695"
+# This Exercism-wide label can be used to bulk-close issues in preparation for pausing community contributions
+- name: "paused"
+ description: "Work paused until further notice"
+ color: "e4e669"
+
# ----------------------------------------------------------------------------------------- #
# These are the repository-specific labels that augment the Exercise-wide labels defined in #
# https://github.com/exercism/org-wide-files/blob/main/global-files/.github/labels.yml. #
diff --git a/.github/pr-commenter.yml b/.github/pr-commenter.yml
deleted file mode 100644
index 29441a8251..0000000000
--- a/.github/pr-commenter.yml
+++ /dev/null
@@ -1,68 +0,0 @@
-comment:
- on-update: edit
- glob-options:
- dot: true
-
- header: |
- ### Dear {{ prAuthor }}
- Thank you for contributing to the JavaScript track on Exercism! 💙
- You will see some automated feedback below 🤖. It would be great if you can make sure your PR covers those points. This will save your reviewer some time and your change can be merged quicker.
-
- footer: |
-
- Automated comment created by [PR Commenter](https://github.com/exercism/pr-commenter-action) 🤖.
-
- snippets:
- - id: concept-changes
- files:
- - 'exercises/concept/*/.docs/introduction.md'
- - 'concepts/*/introduction.md'
- - 'concepts/*/about.md'
- body: |
- - 📜 The following files usually contain very similar content.
- - `concepts//about.md`
- - `concepts//introduction.md`
- - `exercises/concept//.docs/introduction.md`
-
- Please check whether the changes you made to one of these also need to be applied to the others.
-
- - id: exemplar-or-stub-changes
- files:
- - 'exercises/concept/*/.meta/*.js'
- - any:
- - 'exercises/concept/*/*.js'
- - '!exercises/concept/*/*.spec.js'
- body: |
- - 🧦 If you changed the function signature or the JSDoc comment in the exemplar file (`.meta/exemplar.js`) or the stub file (`.js`), make sure the change is applied to both files.
-
- - id: problem-spec-related
- files:
- - 'exercises/practice/*/*.spec.js'
- - 'exercises/practice/*/.docs/instructions.md'
- body: |
- - ⬆️ The instructions and test cases for many practice exercises originate in the Exercism-wide [problem-specifications repo](https://github.com/exercism/problem-specifications). If the exercise you changed is listed there in the [exercises folder](https://github.com/exercism/problem-specifications/tree/main/exercises), please consider the following.
- - Improvements to the `instructions.md` file should be made in the problem-spec repo so that all tracks can benefit.
- You can open a PR there instead.
- - If you want to add some language specific information, use the `instructions.append.md` file (see [Practice Exercise Docs](https://exercism.org/docs/building/tracks/practice-exercises)).
- - Test cases selected in the `.meta/tests.toml` file need to be implemented in the `.spec.js` file according to the specification in [`canonical-data.json`](https://github.com/exercism/problem-specifications#test-data-canonical-datajson).
-
- - id: note-for-contributors
- files:
- - '*'
- - '**/*'
- body: |
- - ✍️ If your PR is not related to an existing issue (and is not self-explaining like a typo fix), please make sure the description explains why the change you made is necessary.
-
- - 🔤 If your PR fixes an easy to identify typo, if would be great if you could check for that typo in the whole repo. For example, if you found `Unicdoe`, use "replace all" in your editor (or command line magic) to fix it consistently.
-
- - id: note-for-maintainers
- files:
- - '*'
- - '**/*'
- body: |
- ### Dear Reviewer/Maintainer
- - 📏 Make sure you set the appropriate **`x:size` label** for the PR. (This also works after merging, in case you forgot about it.)
-
- - 🔍 **Don't be too nit-picky.** If the PR is a clear improvement compared to the status quo, it should be approved as clear signal this is good to be merged even if the minor comments you might have are not addressed by the contributor. Further improvement ideas can be captured in issues (if important enough) and implemented via additional PRs.
-
- - 🤔 After reviewing the diff in the "Files changed" section, take a moment to think about whether there are **changes missing** from the diff. Does something need to be adjusted in other places so the code or content stays consistent?
diff --git a/.github/workflows/action-format.yml b/.github/workflows/action-format.yml
index 2a8fb0c43f..8026e38ebb 100644
--- a/.github/workflows/action-format.yml
+++ b/.github/workflows/action-format.yml
@@ -3,20 +3,21 @@ name: 'javascript / format'
on:
issue_comment:
types: [created]
+ workflow_dispatch:
jobs:
format:
name: 'Format code'
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/format')
steps:
- name: 'Post acknowledgement that it will format code'
continue-on-error: true # Never fail the build if this fails
- uses: actions/github-script@7a5c598405937d486b0331594b5da2b14db670da
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
- github.issues.createComment({
+ github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
@@ -34,7 +35,7 @@ jobs:
id: fork_status
run: |
IS_FORK="$(jq '.head.repo.fork' "/tmp/pr.json")"
- echo "::set-output name=fork::$IS_FORK"
+ echo "fork=$IS_FORK" >> "$GITHUB_OUTPUT"
- name: 'Setup SSH deploy key'
if: steps.fork_status.outputs.fork == 'false'
@@ -60,12 +61,18 @@ jobs:
git clone $HEAD_REPO .
git checkout -b "$HEAD_REF" "origin/$HEAD_REF"
- - name: Use Node.js LTS (16.x)
- uses: actions/setup-node@2fddd8803e2f5c9604345a0b591c3020ee971a93
+ - name: Enable corepack to fix https://github.com/actions/setup-node/pull/901
+ run: corepack enable pnpm
+
+ - name: Use Node.js LTS (22.x)
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
- node-version: '16'
+ node-version: 22.x
+ cache: 'pnpm'
+
- name: Install project development dependencies
- run: npm install --no-save
+ run: corepack pnpm install --no-save
+
- name: 'Format code'
run: ./bin/format.sh
@@ -88,11 +95,11 @@ jobs:
- name: 'Post acknowledgement that it has formatted the code'
continue-on-error: true # Never fail the build if this fails
- uses: actions/github-script@7a5c598405937d486b0331594b5da2b14db670da
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
- github.issues.createComment({
+ github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
@@ -102,11 +109,11 @@ jobs:
- name: 'Post reminder to trigger build manually'
continue-on-error: true # Never fail the build if this fails
if: steps.fork_status.outputs.fork == 'true'
- uses: actions/github-script@7a5c598405937d486b0331594b5da2b14db670da
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
- github.issues.createComment({
+ github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
diff --git a/.github/workflows/action-sync.yml b/.github/workflows/action-sync.yml
index 19875dcf67..7ea0a2a96f 100644
--- a/.github/workflows/action-sync.yml
+++ b/.github/workflows/action-sync.yml
@@ -3,20 +3,21 @@ name: 'javascript / sync'
on:
issue_comment:
types: [created]
+ workflow_dispatch:
jobs:
format:
name: 'Sync all exercises'
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/sync')
steps:
- name: 'Post acknowledgement that it will sync exercises'
continue-on-error: true # Never fail the build if this fails
- uses: actions/github-script@7a5c598405937d486b0331594b5da2b14db670da
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
- github.issues.createComment({
+ github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
@@ -34,7 +35,7 @@ jobs:
id: fork_status
run: |
IS_FORK="$(jq '.head.repo.fork' "/tmp/pr.json")"
- echo "::set-output name=fork::$IS_FORK"
+ echo "fork=$IS_FORK" >> "$GITHUB_OUTPUT"
- name: 'Setup SSH deploy key'
if: steps.fork_status.outputs.fork == 'false'
@@ -61,9 +62,11 @@ jobs:
git checkout -b "$HEAD_REF" "origin/$HEAD_REF"
- name: 'Install dependencies'
- run: yarn install
+ run: |
+ corepack enable pnpm
+ corepack pnpm install
- name: 'Sync exercises'
- run: npx babel-node scripts/sync
+ run: corepack pnpm node scripts/sync.mjs
- name: 'Commit changes'
run: |
@@ -84,11 +87,11 @@ jobs:
- name: 'Post acknowledgement that it has synced the code'
continue-on-error: true # Never fail the build if this fails
- uses: actions/github-script@7a5c598405937d486b0331594b5da2b14db670da
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
- github.issues.createComment({
+ github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
@@ -98,11 +101,11 @@ jobs:
- name: 'Post reminder to trigger build manually'
continue-on-error: true # Never fail the build if this fails
if: steps.fork_status.outputs.fork == 'true'
- uses: actions/github-script@7a5c598405937d486b0331594b5da2b14db670da
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
- github.issues.createComment({
+ github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
diff --git a/.github/workflows/ci.js.yml b/.github/workflows/ci.js.yml
index 1c604546a4..12b4dbcd1a 100644
--- a/.github/workflows/ci.js.yml
+++ b/.github/workflows/ci.js.yml
@@ -6,40 +6,49 @@ name: javascript / main
on:
push:
branches: [main]
+ workflow_dispatch:
jobs:
precheck:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
steps:
- - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
- - name: Use Node.js LTS (16.x)
- uses: actions/setup-node@2fddd8803e2f5c9604345a0b591c3020ee971a93
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
+ - name: Enable corepack to fix https://github.com/actions/setup-node/pull/901
+ run: corepack enable pnpm
+
+ - name: Use Node.js LTS (22.x)
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
- node-version: '16'
+ node-version: 22.x
+ cache: 'pnpm'
- name: Install project dependencies
- run: npm ci
+ run: corepack pnpm install --frozen-lockfile
- name: Run exercism/javascript ci precheck (checks config, lint code, and runs tests) for all exercises
- run: npx babel-node scripts/ci-check
+ run: corepack pnpm node scripts/ci-check.mjs
ci:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
strategy:
matrix:
- node-version: ['16', '17', '18']
+ node-version: [22.x]
steps:
- - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
+ - name: Enable corepack to fix https://github.com/actions/setup-node/pull/901
+ run: corepack enable pnpm
+
- name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@2fddd8803e2f5c9604345a0b591c3020ee971a93
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: ${{ matrix.node-version }}
+ cache: 'pnpm'
- name: Install project dependencies
- run: npm ci
+ run: corepack pnpm install --frozen-lockfile
- name: Run exercism/javascript ci (checks config, lint code, and runs tests) for all exercises
- run: npx babel-node scripts/ci
+ run: corepack pnpm node scripts/ci.mjs
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 7dcf268fb0..de107554de 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -16,7 +16,7 @@ on:
jobs:
analyze:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
strategy:
fail-fast: false
@@ -29,11 +29,11 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@v2
+ uses: github/codeql-action/init@v4.37.3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -44,7 +44,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- uses: github/codeql-action/autobuild@v2
+ uses: github/codeql-action/autobuild@v4.37.3
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -58,4 +58,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v2
+ uses: github/codeql-action/analyze@v4.37.3
diff --git a/.github/workflows/no-important-files-changed.yml b/.github/workflows/no-important-files-changed.yml
new file mode 100644
index 0000000000..812e912966
--- /dev/null
+++ b/.github/workflows/no-important-files-changed.yml
@@ -0,0 +1,23 @@
+name: No important files changed
+
+on:
+ 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:
+ check:
+ 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 }}
diff --git a/.github/workflows/pause-community-contributions.yml b/.github/workflows/pause-community-contributions.yml
new file mode 100644
index 0000000000..77636a2eb4
--- /dev/null
+++ b/.github/workflows/pause-community-contributions.yml
@@ -0,0 +1,25 @@
+name: Pause Community Contributions
+
+on:
+ issues:
+ types:
+ - opened
+ pull_request_target:
+ types:
+ - opened
+ paths-ignore:
+ - 'exercises/*/*/.approaches/**'
+ - 'exercises/*/*/.articles/**'
+
+permissions:
+ issues: write
+ pull-requests: write
+
+jobs:
+ pause:
+ if: github.repository_owner == 'exercism' # Stops this job from running on forks
+ uses: exercism/github-actions/.github/workflows/community-contributions.yml@main
+ with:
+ forum_category: javascript
+ secrets:
+ github_membership_token: ${{ secrets.COMMUNITY_CONTRIBUTIONS_WORKFLOW_TOKEN }}
diff --git a/.github/workflows/ping-cross-track-maintainers-team.yml b/.github/workflows/ping-cross-track-maintainers-team.yml
new file mode 100644
index 0000000000..b6ec9c5662
--- /dev/null
+++ b/.github/workflows/ping-cross-track-maintainers-team.yml
@@ -0,0 +1,16 @@
+name: Ping cross-track maintainers team
+
+on:
+ pull_request_target:
+ types:
+ - opened
+
+permissions:
+ pull-requests: write
+
+jobs:
+ ping:
+ if: github.repository_owner == 'exercism' # Stops this job from running on forks
+ uses: exercism/github-actions/.github/workflows/ping-cross-track-maintainers-team.yml@main
+ secrets:
+ github_membership_token: ${{ secrets.COMMUNITY_CONTRIBUTIONS_WORKFLOW_TOKEN }}
diff --git a/.github/workflows/pr-comment.yml b/.github/workflows/pr-comment.yml
deleted file mode 100644
index 321bbae256..0000000000
--- a/.github/workflows/pr-comment.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-name: PR Comment
-
-on: [pull_request_target]
-
-jobs:
- pr-comment:
- runs-on: ubuntu-latest
- steps:
- - uses: exercism/pr-commenter-action@3706a61d7c6cf1a77db576b9a4c0e7046ae78f1a
- with:
- github-token: '${{ github.token }}'
- config-file: '.github/pr-commenter.yml'
- template-variables: |
- {
- "prAuthor": "${{ github.event.pull_request.user.login }}"
- }
diff --git a/.github/workflows/pr.ci.js.yml b/.github/workflows/pr.ci.js.yml
index 415225ff95..c15da4725f 100644
--- a/.github/workflows/pr.ci.js.yml
+++ b/.github/workflows/pr.ci.js.yml
@@ -7,49 +7,71 @@ on: pull_request
jobs:
precheck:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
steps:
- name: Checkout PR
- uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
+ with:
+ fetch-depth: ${{ github.event_name == 'pull_request' && 2 || 0 }}
+
+ - name: Get changed files
+ id: changed-files
+ run: |
+ if ${{ github.event_name == 'pull_request' }}; then
+ echo "changed_files=$(git diff --name-only --diff-filter=ACM -r HEAD^1 HEAD | xargs)" >> $GITHUB_OUTPUT
+ else
+ echo "changed_files=$(git diff --name-only --diff-filter=ACM ${{ github.event.before }} ${{ github.event.after }} | xargs)" >> $GITHUB_OUTPUT
+ fi
- - name: Use Node.js LTS (16.x)
- uses: actions/setup-node@2fddd8803e2f5c9604345a0b591c3020ee971a93
+ - name: Enable corepack to fix https://github.com/actions/setup-node/pull/901
+ run: corepack enable pnpm
+
+ - name: Use Node.js LTS (22.x)
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
- node-version: '16'
+ node-version: 22.x
+ cache: 'pnpm'
- name: Install project dependencies
- run: npm ci
+ run: corepack pnpm install --frozen-lockfile
- name: Run exercism/javascript ci precheck (stub files, config integrity) for changed exercises
- run: |
- PULL_REQUEST_URL=$(jq -r ".pull_request.url" "$GITHUB_EVENT_PATH")
- curl --url $"${PULL_REQUEST_URL}/files?per_page=100" --header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' | \
- jq -c '.[] | select(.status == "added" or .status == "modified") | select(.filename | match("\\.(js|jsx|md|json)$")) | .filename' | \
- xargs -r npx babel-node scripts/pr-check
+ run: corepack pnpm node scripts/pr.mjs ${{ steps.changed-files.outputs.changed_files }}
ci:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
strategy:
matrix:
- node-version: ['16', '17', '18']
+ node-version: [22.x]
steps:
- name: Checkout PR
- uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
+ with:
+ fetch-depth: ${{ github.event_name == 'pull_request' && 2 || 0 }}
+
+ - name: Get changed files
+ id: changed-files
+ run: |
+ if ${{ github.event_name == 'pull_request' }}; then
+ echo "changed_files=$(git diff --name-only --diff-filter=ACM -r HEAD^1 HEAD | xargs)" >> $GITHUB_OUTPUT
+ else
+ echo "changed_files=$(git diff --name-only --diff-filter=ACM ${{ github.event.before }} ${{ github.event.after }} | xargs)" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Enable corepack to fix https://github.com/actions/setup-node/pull/901
+ run: corepack enable pnpm
- name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@2fddd8803e2f5c9604345a0b591c3020ee971a93
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: ${{ matrix.node-version }}
+ cache: 'pnpm'
- name: Install project dependencies
- run: npm ci
+ run: corepack pnpm install --frozen-lockfile
- name: Run exercism/javascript ci (runs tests) for changed/added exercises
- run: |
- PULL_REQUEST_URL=$(jq -r ".pull_request.url" "$GITHUB_EVENT_PATH")
- curl --url $"${PULL_REQUEST_URL}/files?per_page=100" --header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' | \
- jq -c '.[] | select(.status == "added" or .status == "modified") | select(.filename | match("\\.(js|jsx|md|json)$")) | .filename' | \
- xargs -r npx babel-node scripts/pr
+ run: corepack pnpm node scripts/pr.mjs ${{ steps.changed-files.outputs.changed_files }}
diff --git a/.github/workflows/run-configlet-sync.yml b/.github/workflows/run-configlet-sync.yml
new file mode 100644
index 0000000000..b49cbffe81
--- /dev/null
+++ b/.github/workflows/run-configlet-sync.yml
@@ -0,0 +1,10 @@
+name: Run Configlet Sync
+
+on:
+ workflow_dispatch:
+ schedule:
+ - cron: '0 0 15 * *'
+
+jobs:
+ call-gha-workflow:
+ uses: exercism/github-actions/.github/workflows/configlet-sync.yml@main
diff --git a/.github/workflows/verify-code-formatting.yml b/.github/workflows/verify-code-formatting.yml
index fc3a5255d2..3e4471371b 100644
--- a/.github/workflows/verify-code-formatting.yml
+++ b/.github/workflows/verify-code-formatting.yml
@@ -7,15 +7,10 @@ on:
jobs:
verify:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
steps:
- name: 'Checkout code'
- uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
- - name: Use Node.js LTS (16.x)
- uses: actions/setup-node@2fddd8803e2f5c9604345a0b591c3020ee971a93
- with:
- node-version: '16'
- - name: Install project development dependencies
- run: npm i
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
+
- name: 'Verify formatting of all files'
run: ./bin/check-formatting.sh
diff --git a/.gitignore b/.gitignore
index 31c57dd53a..0c88ff6ec3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
/node_modules
/bin/configlet
/bin/configlet.exe
-/pnpm-lock.yaml
+/package-lock.json
/yarn.lock
diff --git a/.prettierignore b/.prettierignore
index 8e55617028..0187784ddc 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,11 +1,24 @@
/.github/labels.yml
-/.github/workflows/sync-labels.yml
+
+# Generated
exercises/**/README.md
+pnpm-lock.yaml
+
!/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
+exercises/**/.approaches/config.json
+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
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 47c6a8b260..3344c414bc 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -7,5 +7,6 @@
// because of how whitespace is (not) rendered.
65
]
- }
+ },
+ "cSpell.words": ["reorderd"]
}
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index df8e36761c..3f7813de10 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -90,4 +90,4 @@ This policy was initially adopted from the Front-end London Slack community and
A version history can be seen on [GitHub](https://github.com/exercism/website-copy/edit/main/pages/code_of_conduct.md).
_This policy is a "living" document, and subject to refinement and expansion in the future.
-This policy applies to the Exercism website, the Exercism GitHub organization, any other Exercism-related communication channels (e.g. Slack, Twitter, email) and any other Exercism entity or event._
+This policy applies to the Exercism website, the Exercism GitHub organization, any other Exercism-related communication channels (e.g. Discord, Forum, Twitter, email) and any other Exercism entity or event._
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 212cfc67c4..89fa61f770 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -4,9 +4,17 @@ This is the JavaScript track, one of the many tracks on [exercism][web-exercism]
You can find this in the [`config.json`][file-config].
It's not uncommon that people discover incorrect implementations of certain tests, have a suggestion for a track-specific hint to aid the student on the _JavaScript specifics_, see optimisations in terms of the configurations of `jest`, `eslint` or other dependencies, report missing edge cases, factual errors, logical errors, and, implement exercises or develop new exercises.
+> [!CAUTION]
+>
+> Please see https://github.com/exercism/javascript/issues/2144.
+>
+> All contributions _MUST_ preceed by opening a topic on the forum.
+> This is for both new content and fixes/changes to old content.
+
We welcome contributions of all sorts and sizes, from reporting issues to submitting patches, as well as joining the current [discussions 💬][issue-discussion].
-> :warning: This guide is slightly outdated and doesn't hold the V3 changes yet.
+> [!WARNING]
+> This guide is slightly outdated and doesn't hold the V3 changes yet.
---
@@ -76,18 +84,18 @@ The baseline of work is as follows:
1. We'll assign the issue to you, so you get to work on this exercise
1. Create a new folder in `/exercises`
1. You'll need to sync this folder with the matching config files.
- You can use `scripts/sync` to do this: `ASSIGNMENT=slug npx babel-node scripts/sync`.
+ You can use `scripts/sync` to do this.
1. Create a `.js` stub file.
1. Create a `.spec.js` test file. Here add the tests, per canonical data if possible (more on canonical data below).
1. Create an `example.js` file. Place a working implementation, assuming it's renamed to `.js`
1. Create `.meta/tests.toml`.
If the exercise that is being implemented has test data in the [problem specifications repository][problem-specifications], the contents of this file **must** be a list of UUIDs of the tests that are implemented or not implemented.
Scroll down to [tools](#tools) to find configlet which aids in generating this file _interactively_.
-1. Run the tests locally, using `scripts/test`: `ASSIGNMENT=slug npx babel-node scripts/test`.
-1. Run the linter locally, using `scripts/lint`: `ASSIGNMENT=slug npx babel-node scripts/lint`.
+1. Run the tests locally, using `scripts/test`.
+1. Run the linter locally, using `scripts/lint`.
1. Create an entry in `config.json`: a unique _new_ UUID (you can use the `configlet uuid` tool to generate one, scroll down to [tools](#tools) to see how you can get it), give it a difficulty (should be similar to similar exercises), and make sure the _order_ of the file is sane.
Currently, the file is ordered first on concept exercise, then on "original core", finally everything else, on difficulty low to high, and ultimately lexicographically.
-1. Format the files, using `scripts/format`: `npx babel-node scripts/format`.
+1. Format the files, using `scripts/format`.
The final step is opening a Pull Request, with these items all checked off.
Make sure the tests run and the linter is happy. It will run automatically on your PR.
@@ -111,6 +119,9 @@ Don't worry! You're not alone in this.
There are always improvements possible on existing exercises.
+> [!IMPORTANT]
+> If you are opening a PR, please ensure you are _on a branch in your fork_, or our automated tooling will not work as expected.
+
#### Improving the README.md
For _practice_ exercises, `README.md` is generated from a canonical source.
@@ -167,7 +178,7 @@ You may improve these files by making the required changes and opening a new Pul
## Tools
You'll need LTS or higher Node.js to contribute to the _code_ in this repository.
-Run `npm install` in the root to be able to run the scripts as listed below.
+Run `corepack pnpm install` in the root to be able to run the scripts as listed below.
We use the following dependencies:
- `shelljs` to provide shell interface to scripts
@@ -176,9 +187,9 @@ We use the following dependencies:
- `babel` to transpile everything so it works _regardless of your version of Node.js_.
We also use `prettier` to format the files.
-**Prettier is installed when using `npm install`**.
-You may use `npx babel-node scripts/format` to run prettier.
-If you want to auto-format using your editor, install via `npm install` and it will Just Work™.
+**Prettier is installed when using `corepack pnpm install`**.
+You may use `corepack pnpm node scripts/format.mjs` to run prettier.
+If you want to auto-format using your editor, install via `corepack pnpm install` and it will Just Work™.
### Fetch configlet
@@ -209,31 +220,11 @@ It then interactively gives the maintainer the option to include or exclude test
We have various `scripts` to aid with maintaining and contributing to this repository.
-> ⚠ If you encounter the following error:
->
-> ```text
-> SyntaxError: Unexpected token 'export'
-> ```
->
-> It's because your local Node.js version does **not** support es6
-> `import` and `export` statements in regular `.js` files, or
-> files without extension. This is one of the reasons why these
-> scripts are meant to be run through Node.js:
->
-> ```shell
-> npx babel-node scripts/the-script
-> ```
->
-> Additionally, this ensures that the code written in the scripts
-> and their dependencies can be executed by your current Node.js
-> version, which may be different from the version used by the
-> maintainer or contributor who contributed to the script.
-
#### `format`
```js
/*
- * Run this script (from root directory): npx babel-node scripts/format
+ * Run this script (from root directory): corepack pnpm node scripts/format.mjs
*
* This runs `prettier` on all applicable files, FORCES using the same version
* as the CI uses to check if the files have been formatted.
@@ -241,14 +232,14 @@ We have various `scripts` to aid with maintaining and contributing to this repos
```
Use this action to format all the files using the correct version of prettier.
-If you want your editor to do this automatically, install the project development dependencies (`npm i`), which includes `prettier`.
+If you want your editor to do this automatically, install the project development dependencies (`corepack pnpm install`), which includes `prettier`.
The correct version will be extracted when running `.github/workflows/verify-code-formatting.yml`.
#### `lint`
```js
/*
- * Run this script (from root directory): npx babel-node scripts/lint
+ * Run this script (from root directory): corepack pnpm node scripts/lint.mjs
*
* This runs `eslint` on all sample solutions (and test) files
*/
@@ -258,14 +249,24 @@ If the `ASSIGNMENT` environment variable is set, only _that_ exercise is tested.
For example, if you only want to lint `two-fer`, you may, depending on your environment use:
```shell
-ASSIGNMENT=two-fer npx babel-node scripts/lint
+ASSIGNMENT=practice/two-fer corepack pnpm node scripts/lint.mjs
+```
+
+Note: on Windows, if you're not in a POSIX style command line, you can use `cross-env` to make this work:
+
+```shell
+# if installed globally
+cross-env ASSIGNMENT=practice/two-fer corepack pnpm node scripts/lint.mjs
+
+# otherwise
+corepack pnpm dlx cross-env ASSIGNMENT=practice/two-fer node scripts/lint.mjs
```
#### `test`
```js
/**
- * Run this script (from root directory): npx babel-node scripts/test
+ * Run this script (from root directory): corepack pnpm node scripts/test.mjs
*
* This runs `jest` tests for all sample solutions
*/
@@ -275,14 +276,24 @@ If the `ASSIGNMENT` environment variable is set, only _that_ exercise is tested.
For example, if you only want to test the `example.js` for `two-fer`, you may, depending on your environment, use:
```shell
-ASSIGNMENT=two-fer npx babel-node scripts/test
+ASSIGNMENT=practice/two-fer corepack pnpm node scripts/test.mjs
+```
+
+Note: on Windows, if you're not in a POSIX style command line, you can use `cross-env` to make this work:
+
+```shell
+# if installed globally
+cross-env ASSIGNMENT=practice/two-fer corepack pnpm node scripts/test.mjs
+
+# otherwise
+corepack pnpm dlx cross-env ASSIGNMENT=practice/two-fer node scripts/test.mjs
```
#### `sync`
```js
/**
- * Run this script (from root directory): npx babel-node scripts/sync
+ * Run this script (from root directory): corepack pnpm node scripts/sync.mjs
*
* This script is used to propagate any change to root package.json to
* all exercises and keep them in sync.
@@ -295,14 +306,24 @@ If the `ASSIGNMENT` environment variable is set, only _that_ exercise is tested.
For example, if you only want to sync the files for `two-fer`, you may, depending on your environment, use:
```shell
-ASSIGNMENT=two-fer npx babel-node scripts/sync
+ASSIGNMENT=practice/two-fer corepack pnpm node scripts/sync.mjs
+```
+
+Note: on Windows, if you're not in a POSIX style command line, you can use `cross-env` to make this work:
+
+```shell
+# if installed globally
+cross-env ASSIGNMENT=practice/two-fer corepack pnpm node scripts/sync.mjs
+
+# otherwise
+corepack pnpm dlx cross-env ASSIGNMENT=practice/two-fer node scripts/sync.mjs
```
#### `checksum`
```js
/*
- * Run this script (from root directory): npx babel-node scripts/checksum
+ * Run this script (from root directory): corepack pnpm node scripts/checksum.mjs
*
* This will check root `package.json` matches each exercise's `package.json`.
* But the catch is there are some dependencies that are only used at build-time and not served to end-users
@@ -315,7 +336,7 @@ ASSIGNMENT=two-fer npx babel-node scripts/sync
```js
/**
- * Run this script (from root directory): npx babel-node scripts/ci-check
+ * Run this script (from root directory): corepack pnpm node scripts/ci-check.mjs
*
* This will run the following checks:
*
@@ -331,7 +352,7 @@ Run this script to check stubs, configuration integrity and lint the code.
```js
/**
- * Run this script (from root directory): npx babel-node scripts/ci
+ * Run this script (from root directory): corepack pnpm node scripts/ci.mjs
*
* This will run the following checks:
*
@@ -346,13 +367,13 @@ Run this script to test all exercises.
```js
/**
- * Run this script (from root directory): npx babel-node scripts/name-check
+ * Run this script (from root directory): corepack pnpm node scripts/name-check.mjs
*
* This will run the following checks:
*
* 1. Package name is of the format "@exercism/javascript-"
*
- * This script also allows fixing these names: npx babel-node scripts/name-check --fix
+ * This script also allows fixing these names: corepack pnpm node scripts/name-check.mjs --fix
*/
```
@@ -362,7 +383,7 @@ Run this script to check if the package name in package.json of exercises is in
```js
/**
- * Run this script (from root directory): npx babel-node scripts/name-uniq
+ * Run this script (from root directory): corepack pnpm node scripts/name-uniq.mjs
*
* This will run the following checks:
*
@@ -376,13 +397,13 @@ Run this script to check if there is any duplicate package name.
```js
/**
- * Run this script (from root directory): npx babel-node scripts/directory-check
+ * Run this script (from root directory): corepack pnpm node scripts/directory-check.mjs
*
* This will run the following checks:
*
* 1. The package has the correct directory based on the path to the exercise.
*
- * This script also allows fixing these directories: npx babel-node scripts/directory-check --fix
+ * This script also allows fixing these directories: corepack pnpm node scripts/directory-check.mjs --fix
*/
```
@@ -391,7 +412,17 @@ If the `ASSIGNMENT` environment variable is set, only _that_ exercise is tested.
For example, if you only want to test the directory for `concept/closures`, you may, depending on your environment, use:
```shell
-ASSIGNMENT=concept/closures npx babel-node scripts/directory-check
+ASSIGNMENT=concept/closures corepack pnpm node scripts/directory-check.mjs
+```
+
+Note: on Windows, if you're not in a POSIX style command line, you can use `cross-env` to make this work:
+
+```shell
+# if installed globally
+cross-env ASSIGNMENT=concept/closures corepack pnpm node scripts/directory-check.mjs
+
+# otherwise
+corepack pnpm dlx cross-env ASSIGNMENT=concept/closures node scripts/directory-check.mjs
```
[configlet]: https://exercism.org/docs/building/configlet
diff --git a/README.md b/README.md
index 7a0b2a3a70..7fa8de99f4 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,11 @@
# Exercism JavaScript Track
-[](https://github.com/exercism/javascript/actions?query=workflow%3Aconfiglet) [](https://github.com/exercism/javascript/actions?query=workflow%3A%22javascript+%2F+main%22)
+[](https://github.com/exercism/javascript/actions/workflows/configlet.yml) [](https://github.com/exercism/javascript/actions?query=workflow%3A%22javascript+%2F+main%22)
+
+> [!IMPORTANT]
+> We 💙 our community but **this repository does not accept community contributions at this time**.
+> There are no active maintainers to review PRs.
+> Please read this [community blog post][freeing-maintainers] for details.
**Exercism exercises in JavaScript**
@@ -16,10 +21,10 @@ It also has a list of tools you can use, of which the `test` tool is one of them
## Running the code quality tooling (linter)
-This run `eslint` for all files that _require_ linting.
+This runs `eslint` for all files that _require_ linting.
```shell
-npx eslint exercises/**/*.spec.js exercises/**/.meta/*.js --fix
+corepack pnpm node scripts/lint.mjs --fix
```
These are also the files that are linted using the lint script, mentioned in [CONTRIBUTING.md][file-contributing].
@@ -43,21 +48,25 @@ This runs `jest` tests for all sample solutions.
This _does not_ use the regular way to run `jest`, because the example solution files must be renamed to be imported correctly into the test files.
```shell
-npx babel-node scripts/test
+corepack pnpm node scripts/test.mjs
```
If the `ASSIGNMENT` environment variable is set, only _that_ exercise is tested.
For example, if you only want to test the `example.js` for the practice exercise `two-fer`, you may, depending on your environment, use:
```shell
-ASSIGNMENT=practice/two-fer npx babel-node scripts/test
+ASSIGNMENT=practice/two-fer corepack pnpm node scripts/test.mjs
```
> Running on Windows? Depending on your shell, environment variables are set differently.
> You can use `cross-env` to normalize this. The following should work across environments:
>
> ```bash
-> npx cross-env ASSIGNMENT=practice/two-fer babel-node scripts/test
+> # if installed globally
+> cross-env ASSIGNMENT=practice/two-fer corepack pnpm node scripts/test.mjs
+>
+> # otherwise
+> corepack pnpm dlx cross-env ASSIGNMENT=practice/two-fer node scripts/test.mjs
> ```
## Related repositories
@@ -93,3 +102,4 @@ A lot of the improvements made to this track and tooling are also made to the Ty
[git-typescript-representer]: https://github.com/exercism/typescript-representer
[git-typescript-test-runner]: https://github.com/exercism/typescript-test-runner
[git-website-copy]: https://github.com/exercism/website-copy
+[freeing-maintainers]: https://exercism.org/blog/freeing-our-maintainers
diff --git a/babel.config.js b/babel.config.js
index b781d5a667..a638497df1 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/bin/check-formatting.sh b/bin/check-formatting.sh
index a444a712b7..5379360c0e 100755
--- a/bin/check-formatting.sh
+++ b/bin/check-formatting.sh
@@ -1,27 +1,50 @@
-#!/bin/bash
+#!/usr/bin/env bash
-if [ -z "$EXERCISM_PRETTIER_VERSION" ]; then
- echo "Pulling prettier version from package.json"
- EXERCISM_PRETTIER_VERSION=$(npm list prettier | grep -Po '.*\sprettier@\K.*')
+set -uo pipefail
+
+if [ -z "${EXERCISM_PRETTIER_VERSION:-}" ]; then
+ echo "[format] pulling prettier version from pnpm list using sed"
+ EXERCISM_PRETTIER_VERSION="$(corepack pnpm list prettier --parseable | sed -n -e '1,$s/^.*prettier@//' -e 's/\\node_modules\\prettier//p')"
+ echo "[format] expected version is now ${EXERCISM_PRETTIER_VERSION:-}"
+fi
+
+if [ -z "${EXERCISM_PRETTIER_VERSION:-}" ]; then
+ echo "[format] pulling prettier version via pnpm-lock.yaml using grep"
+ EXERCISM_PRETTIER_VERSION="$(cat pnpm-lock.yaml | grep -Po ' prettier@\K[^:]+')"
+ echo "[format] expected version is now ${EXERCISM_PRETTIER_VERSION:-}"
fi
-if [ -z "$EXERCISM_PRETTIER_VERSION" ]; then
+if [ -z "${EXERCISM_PRETTIER_VERSION:-}" ]; then
+ echo "Version could not be pulled using sed or grep" >&2
+ echo ""
echo "---------------------------------------------------"
echo "This script requires the EXERCISM_PRETTIER_VERSION variable to work."
echo "Please see https://exercism.org/docs/building/markdown/style-guide for guidance."
echo "---------------------------------------------------"
- echo "This is what npm list reports:"
- echo "$(npm list prettier)"
+ echo "$(corepack pnpm -v)"
+ echo ""
+ echo "This is what corepack pnpm list reports:"
+ echo "$ corepack pnpm list prettier --parseable"
+ echo "$(corepack pnpm list prettier --parseable)"
+ echo ""
+ echo "And corepack pnpm info reports the following:"
+ echo "$ corepack pnpm info prettier"
+ echo "$(corepack pnpm info prettier)"
+ echo ""
+ echo "This is the version that can be extracted using sed:"
+ echo "$ corepack pnpm list prettier --parseable | sed -n -e '1,\$s/^.*prettier@//' -e 's/\\node_modules\\prettier//p'"
+ echo "└─ $(corepack pnpm list prettier --parseable | sed -n -e '1,$s/^.*prettier@//' -e 's/\\node_modules\\prettier//p')"
echo ""
- echo "This is the version that can be extracted:"
- echo "$(npm list prettier | grep -Po '.*\sprettier@\K.*')"
+ echo "This is the version that can be extracted using grep:"
+ echo "$ cat pnpm-lock.yaml | grep -Po ' prettier@\K[^:]+'"
+ echo "└─ $(cat pnpm-lock.yaml | grep -Po ' prettier@\K[^:]+')"
echo ""
echo "These files are found in the repo root:"
echo "$(ls -p | grep -v /)"
echo "---------------------------------------------------"
exit 1
else
- echo "Running format with prettier@$EXERCISM_PRETTIER_VERSION"
+ echo "[format] running with prettier@$EXERCISM_PRETTIER_VERSION"
fi
-npx "prettier@$EXERCISM_PRETTIER_VERSION" --check "**/*.{js,jsx,ts,tsx,css,sass,scss,html,json,md,yml}"
+corepack pnpm dlx "prettier@$EXERCISM_PRETTIER_VERSION" --check "**/*.{js,jsx,cjs,mjs,ts,tsx,css,sass,scss,html,json,md,yml}"
diff --git a/bin/fetch-configlet b/bin/fetch-configlet
index 43f1c83cee..6bef43ab72 100755
--- a/bin/fetch-configlet
+++ b/bin/fetch-configlet
@@ -1,29 +1,10 @@
#!/usr/bin/env bash
-set -eo pipefail
+# This file is a copy of the
+# https://github.com/exercism/configlet/blob/main/scripts/fetch-configlet file.
+# Please submit bugfixes/improvements to the above file to ensure that all tracks benefit from the changes.
-readonly LATEST='https://api.github.com/repos/exercism/configlet/releases/latest'
-
-case "$(uname)" in
- Darwin*) os='mac' ;;
- Linux*) os='linux' ;;
- Windows*) os='windows' ;;
- MINGW*) os='windows' ;;
- MSYS_NT-*) os='windows' ;;
- *) os='linux' ;;
-esac
-
-case "${os}" in
- windows*) ext='zip' ;;
- *) ext='tgz' ;;
-esac
-
-case "$(uname -m)" in
- *64*) arch='64bit' ;;
- *686*) arch='32bit' ;;
- *386*) arch='32bit' ;;
- *) arch='64bit' ;;
-esac
+set -eo pipefail
curlopts=(
--silent
@@ -37,22 +18,74 @@ if [[ -n "${GITHUB_TOKEN}" ]]; then
curlopts+=(--header "authorization: Bearer ${GITHUB_TOKEN}")
fi
-suffix="${os}-${arch}.${ext}"
-
get_download_url() {
- curl "${curlopts[@]}" --header 'Accept: application/vnd.github.v3+json' "${LATEST}" |
+ local os="$1"
+ local ext="$2"
+ local latest='https://api.github.com/repos/exercism/configlet/releases/latest'
+ local arch
+ case "$(uname -m)" in
+ aarch64|arm64) arch='arm64' ;;
+ x86_64) arch='x86-64' ;;
+ *686*) arch='i386' ;;
+ *386*) arch='i386' ;;
+ *) arch='x86-64' ;;
+ esac
+ local suffix="${os}_${arch}.${ext}"
+ curl "${curlopts[@]}" --header 'Accept: application/vnd.github.v3+json' "${latest}" |
grep "\"browser_download_url\": \".*/download/.*/configlet.*${suffix}\"$" |
cut -d'"' -f4
}
-download_url="$(get_download_url)"
-output_dir="bin"
-output_path="${output_dir}/latest-configlet.${ext}"
-curl "${curlopts[@]}" --output "${output_path}" "${download_url}"
+main() {
+ local output_dir
+ if [[ -d ./bin ]]; then
+ output_dir="./bin"
+ elif [[ $PWD == */bin ]]; then
+ output_dir="$PWD"
+ else
+ echo "Error: no ./bin directory found. This script should be ran from a repo root." >&2
+ return 1
+ fi
+
+ local os
+ case "$(uname -s)" in
+ Darwin*) os='macos' ;;
+ Linux*) os='linux' ;;
+ Windows*) os='windows' ;;
+ MINGW*) os='windows' ;;
+ MSYS_NT-*) os='windows' ;;
+ *) os='linux' ;;
+ esac
+
+ local ext
+ case "${os}" in
+ windows) ext='zip' ;;
+ *) ext='tar.gz' ;;
+ esac
-case "${ext}" in
- *zip) unzip "${output_path}" -d "${output_dir}" ;;
- *) tar xzf "${output_path}" -C "${output_dir}" ;;
-esac
+ echo "Fetching configlet..." >&2
+ local download_url
+ download_url="$(get_download_url "${os}" "${ext}")"
+ local output_path="${output_dir}/latest-configlet.${ext}"
+ curl "${curlopts[@]}" --output "${output_path}" "${download_url}"
+
+ case "${ext}" in
+ zip) unzip "${output_path}" -d "${output_dir}" ;;
+ *) tar xzf "${output_path}" -C "${output_dir}" ;;
+ esac
+
+ rm -f "${output_path}"
+
+ local executable_ext
+ case "${os}" in
+ windows) executable_ext='.exe' ;;
+ *) executable_ext='' ;;
+ esac
+
+ local configlet_path="${output_dir}/configlet${executable_ext}"
+ local configlet_version
+ configlet_version="$(${configlet_path} --version)"
+ echo "Downloaded configlet ${configlet_version} to ${configlet_path}"
+}
-rm -f "${output_path}"
+main
diff --git a/bin/fetch-configlet.ps1 b/bin/fetch-configlet.ps1
index 291e57eb22..a7896b2251 100755
--- a/bin/fetch-configlet.ps1
+++ b/bin/fetch-configlet.ps1
@@ -1,26 +1,42 @@
+# This file is a copy of the
+# https://github.com/exercism/configlet/blob/main/scripts/fetch-configlet.ps1 file.
+# Please submit bugfixes/improvements to the above file to ensure that all tracks
+# benefit from the changes.
+
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
$requestOpts = @{
- Headers = If ($env:GITHUB_TOKEN) { @{ Authorization = "Bearer ${env:GITHUB_TOKEN}" } } Else { @{ } }
+ Headers = If ($env:GITHUB_TOKEN) { @{ Authorization = "Bearer ${env:GITHUB_TOKEN}" } } Else { @{ } }
MaximumRetryCount = 3
- RetryIntervalSec = 1
+ RetryIntervalSec = 1
}
-$arch = If ([Environment]::Is64BitOperatingSystem) { "64bit" } Else { "32bit" }
-$fileName = "configlet-windows-$arch.zip"
-
Function Get-DownloadUrl {
+ $arch = If ([Environment]::Is64BitOperatingSystem) { "x86-64" } Else { "i386" }
$latestUrl = "https://api.github.com/repos/exercism/configlet/releases/latest"
- Invoke-RestMethod -Uri $latestUrl -PreserveAuthorizationOnRedirect @requestOpts
- | Select-Object -ExpandProperty assets
- | Where-Object { $_.browser_download_url -match $FileName }
- | Select-Object -ExpandProperty browser_download_url
+ Invoke-RestMethod -Uri $latestUrl -PreserveAuthorizationOnRedirect @requestOpts `
+ | Select-Object -ExpandProperty assets `
+ | Where-Object { $_.name -match "^configlet_.+_windows_${arch}.zip$" } `
+ | Select-Object -ExpandProperty browser_download_url -First 1
}
-$downloadUrl = Get-DownloadUrl
$outputDirectory = "bin"
-$outputFile = Join-Path -Path $outputDirectory -ChildPath $fileName
-Invoke-WebRequest -Uri $downloadUrl -OutFile $outputFile @requestOpts
-Expand-Archive $outputFile -DestinationPath $outputDirectory -Force
-Remove-Item -Path $outputFile
+if (!(Test-Path -Path $outputDirectory)) {
+ Write-Output "Error: no ./bin directory found. This script should be ran from a repo root."
+ exit 1
+}
+
+Write-Output "Fetching configlet..."
+$downloadUrl = Get-DownloadUrl
+$outputFileName = "configlet.zip"
+$outputPath = Join-Path -Path $outputDirectory -ChildPath $outputFileName
+Invoke-WebRequest -Uri $downloadUrl -OutFile $outputPath @requestOpts
+
+$configletPath = Join-Path -Path $outputDirectory -ChildPath "configlet.exe"
+if (Test-Path -Path $configletPath) { Remove-Item -Path $configletPath }
+[System.IO.Compression.ZipFile]::ExtractToDirectory($outputPath, $outputDirectory)
+Remove-Item -Path $outputPath
+
+$configletVersion = (Select-String -Pattern "/releases/download/(.+?)/" -InputObject $downloadUrl -AllMatches).Matches.Groups[1].Value
+Write-Output "Downloaded configlet ${configletVersion} to ${configletPath}"
diff --git a/bin/format.sh b/bin/format.sh
index 0ac8ce94de..162438126a 100755
--- a/bin/format.sh
+++ b/bin/format.sh
@@ -1,27 +1,50 @@
#!/usr/bin/env bash
-if [ -z "$EXERCISM_PRETTIER_VERSION" ]; then
- echo "Pulling prettier version from package.json"
- EXERCISM_PRETTIER_VERSION=$(npm list prettier | grep -Po '.*\sprettier@\K.*')
+set -uo pipefail
+
+if [ -z "${EXERCISM_PRETTIER_VERSION:-}" ]; then
+ echo "[format] pulling prettier version from pnpm list using sed"
+ EXERCISM_PRETTIER_VERSION="$(corepack pnpm list prettier --parseable | sed -n -e '1,$s/^.*prettier@//' -e 's/\\node_modules\\prettier//p')"
+ echo "[format] expected version is now ${EXERCISM_PRETTIER_VERSION:-}"
+fi
+
+if [ -z "${EXERCISM_PRETTIER_VERSION:-}" ]; then
+ echo "[format] pulling prettier version via pnpm-lock.yaml using grep"
+ EXERCISM_PRETTIER_VERSION="$(cat pnpm-lock.yaml | grep -Po ' prettier@\K[^:]+')"
+ echo "[format] expected version is now ${EXERCISM_PRETTIER_VERSION:-}"
fi
-if [ -z "$EXERCISM_PRETTIER_VERSION" ]; then
+if [ -z "${EXERCISM_PRETTIER_VERSION:-}" ]; then
+ echo "Version could not be pulled using sed or grep" >&2
+ echo ""
echo "---------------------------------------------------"
echo "This script requires the EXERCISM_PRETTIER_VERSION variable to work."
echo "Please see https://exercism.org/docs/building/markdown/style-guide for guidance."
echo "---------------------------------------------------"
- echo "This is what npm list reports:"
- echo "$(npm list prettier)"
+ echo "$(corepack pnpm -v)"
+ echo ""
+ echo "This is what corepack pnpm list reports:"
+ echo "$ corepack pnpm list prettier --parseable"
+ echo "$(corepack pnpm list prettier --parseable)"
+ echo ""
+ echo "And corepack pnpm info reports the following:"
+ echo "$ corepack pnpm info prettier"
+ echo "$(corepack pnpm info prettier)"
+ echo ""
+ echo "This is the version that can be extracted using sed:"
+ echo "$ corepack pnpm list prettier --parseable | sed -n -e '1,\$s/^.*prettier@//' -e 's/\\node_modules\\prettier//p'"
+ echo "└─ $(corepack pnpm list prettier --parseable | sed -n -e '1,$s/^.*prettier@//' -e 's/\\node_modules\\prettier//p')"
echo ""
- echo "This is the version that can be extracted:"
- echo "$(npm list prettier | grep -Po '.*\sprettier@\K.*')"
+ echo "This is the version that can be extracted using grep:"
+ echo "$ cat pnpm-lock.yaml | grep -Po ' prettier@\K[^:]+'"
+ echo "└─ $(cat pnpm-lock.yaml | grep -Po ' prettier@\K[^:]+')"
echo ""
echo "These files are found in the repo root:"
echo "$(ls -p | grep -v /)"
echo "---------------------------------------------------"
exit 1
else
- echo "Running format with prettier@$EXERCISM_PRETTIER_VERSION"
+ echo "[format] running with prettier@$EXERCISM_PRETTIER_VERSION"
fi
-npx "prettier@$EXERCISM_PRETTIER_VERSION" --write "**/*.{js,jsx,ts,tsx,css,sass,scss,html,json,md,yml}"
+corepack pnpm dlx "prettier@$EXERCISM_PRETTIER_VERSION" --write "**/*.{js,jsx,cjs,mjs,ts,tsx,css,sass,scss,html,json,md,yml}"
diff --git a/bin/generate-config-tree b/bin/generate-config-tree
index 9a08db2e3a..e4c4bae844 100755
--- a/bin/generate-config-tree
+++ b/bin/generate-config-tree
@@ -1,35 +1,35 @@
#!/usr/bin/env node
-const { exercises } = require('../config.json')
-const TAG_CORE = '__core'
-const TAG_BONUS = '__bonus'
+const { exercises } = require('../config.json');
+const TAG_CORE = '__core';
+const TAG_BONUS = '__bonus';
// node inter-opt exports
-exports.TAG_CORE = TAG_CORE
-exports.TAG_BONUS = TAG_BONUS
+exports.TAG_CORE = TAG_CORE;
+exports.TAG_BONUS = TAG_BONUS;
exports.tree = exercises.reduce((result, exercise) => {
- const tag = exercise.slug
+ const tag = exercise.slug;
const item = {
slug: tag,
difficulty: exercise.difficulty,
- }
+ };
if (exercise.core) {
- const current = result[TAG_CORE] || []
+ const current = result[TAG_CORE] || [];
if (result[tag]) {
- console.warn(`${tag} is not ordered correctly in config.json`)
+ console.warn(`${tag} is not ordered correctly in config.json`);
}
return {
...result,
__core: current.concat([item]),
- [tag]: result[tag] || []
- }
+ [tag]: result[tag] || [],
+ };
}
- const parent = exercise.unlocked_by || TAG_BONUS
- const current = result[parent] || []
- return { ...result, [parent]: current.concat([item]) }
-}, {})
+ const parent = exercise.unlocked_by || TAG_BONUS;
+ const current = result[parent] || [];
+ return { ...result, [parent]: current.concat([item]) };
+}, {});
diff --git a/bin/lint.sh b/bin/lint.sh
new file mode 100644
index 0000000000..2ddf019d16
--- /dev/null
+++ b/bin/lint.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+corepack enable pnpm
+corepack pnpm lint
diff --git a/bin/print-config-tree b/bin/print-config-tree
index 337c2c09fd..b4061b19bc 100755
--- a/bin/print-config-tree
+++ b/bin/print-config-tree
@@ -1,34 +1,34 @@
#!/usr/bin/env node
-const actions = require('./generate-config-tree')
+const actions = require('./generate-config-tree');
-const { tree, TAG_BONUS, TAG_CORE } = actions
-const { [TAG_BONUS]: __bonus, [TAG_CORE]: __core, ...track } = tree
+const { tree, TAG_BONUS, TAG_CORE } = actions;
+const { [TAG_BONUS]: __bonus, [TAG_CORE]: __core, ...track } = tree;
function printLn(line) {
- process.stdout.write(`${line}\n`)
+ process.stdout.write(`${line}\n`);
}
function printList(items) {
- items.forEach(item => {
- printLn(`- ${item.slug} (${item.difficulty})`)
- })
+ items.forEach((item) => {
+ printLn(`- ${item.slug} (${item.difficulty})`);
+ });
}
-printLn('Core (matches config.json) of this track:')
-printList(__core)
-printLn('\n')
-printLn('core')
-printLn('----')
-Object.keys(track).forEach(slug => {
- printLn(`├─ ${slug}`)
+printLn('Core (matches config.json) of this track:');
+printList(__core);
+printLn('\n');
+printLn('core');
+printLn('----');
+Object.keys(track).forEach((slug) => {
+ printLn(`├─ ${slug}`);
track[slug].forEach((side, index, self) => {
- junction = index === self.length - 1 ? '└─' : '├─'
- printLn(`│ ${junction} ${side.slug} (${side.difficulty})`)
- })
- printLn('│')
-})
+ junction = index === self.length - 1 ? '└─' : '├─';
+ printLn(`│ ${junction} ${side.slug} (${side.difficulty})`);
+ });
+ printLn('│');
+});
-printLn('bonus')
-printLn('----')
-printList(__bonus)
+printLn('bonus');
+printLn('----');
+printList(__bonus);
diff --git a/concepts/arithmetic-operators/about.md b/concepts/arithmetic-operators/about.md
index 93d16ca627..9bb4e8cb43 100644
--- a/concepts/arithmetic-operators/about.md
+++ b/concepts/arithmetic-operators/about.md
@@ -15,7 +15,7 @@ JavaScript provides 6 different operators to perform basic arithmetic operations
```javascript
19 - 2; // => 17
- 7.4 - 1.2; // => 1.5
+ 7.4 - 1.2; // => 6.2
```
- `*`: The multiplication operator is used to find the product of two numbers
diff --git a/concepts/array-analysis/introduction.md b/concepts/array-analysis/introduction.md
index ac05aa7a55..23eaee54be 100644
--- a/concepts/array-analysis/introduction.md
+++ b/concepts/array-analysis/introduction.md
@@ -83,9 +83,13 @@ numbers.findIndex((num) => num > 9);
```
[^1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes
+
[^2]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every
+
[^3]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some
+
[^4]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find
+
[^5]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex
[predicate_in_programming]: https://derk-jan.com/2020/05/predicate/
diff --git a/concepts/array-destructuring/about.md b/concepts/array-destructuring/about.md
index 4a550871ca..481e84157a 100644
--- a/concepts/array-destructuring/about.md
+++ b/concepts/array-destructuring/about.md
@@ -1,6 +1,6 @@
# About
-Array [destructuring assignment][array_destructuring_docs] is a concise way of extracting values from an array. Its syntax is similar to an [array literal][array_literal_resource] expression, but on the left-hand side of the assignment instead of the right.
+Array [destructuring assignment][mdn-array-destructuring] is a concise way of extracting values from an array. Its syntax is similar to an [array literal][mdn-array-literal] expression, but on the left-hand side of the assignment instead of the right.
```javascript
const frenchNumbers = ['quatre-vingts', 'quatre-vingt-dix', 'cent'];
@@ -14,6 +14,8 @@ french100;
// => 'cent'
```
+## Re-assignment
+
Because variables are mapped to values in the array by position, destructuring syntax can be used to assign or re-assign multiple variables in a single expression.
```javascript
@@ -40,6 +42,8 @@ c;
// => 'purple'
```
+## Skipping assignment
+
The syntax allows skipping values when mapping, for example to ignore specific positions in the array.
In the example below, imagine we have a `getUserInfo` function that returns an array containing a user's first name, last name, and street address.
@@ -53,6 +57,8 @@ streetAddress;
// => "Sunny Lane 523"
```
+## Dropping values
+
The assignment is also not required to use all the values.
```javascript
@@ -65,6 +71,8 @@ lastName;
// => "Noir"
```
+## Taking more values than available
+
It's even possible to extract _more_ values than the array contains; the leftover variables will be assigned `undefined`. This may be useful when the amount of values isn't known ahead of time.
```javascript
@@ -84,6 +92,8 @@ fourth;
// => undefined
```
+## Default values
+
The array destructuring assignment can provide _default values_ in case there is none in the source array.
```javascript
@@ -96,4 +106,10 @@ fourth;
// => undefined
```
+## Related concepts
+
+[concept:javascript/object-destructuring]()
[concept:javascript/rest-and-spread]()
+
+[mdn-array-destructuring]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment
+[mdn-array-literal]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#Creating_an_array
diff --git a/concepts/array-transformations/about.md b/concepts/array-transformations/about.md
index 814fb8d72b..d525810b55 100644
--- a/concepts/array-transformations/about.md
+++ b/concepts/array-transformations/about.md
@@ -61,7 +61,7 @@ arr.reduce(
return accumulator;
},
- { even: [], odd: [] }
+ { even: [], odd: [] },
);
// => { even: [2, 4], odd: [1, 3] }
```
@@ -81,7 +81,7 @@ This method modifies the array it is called on.
### slice (pure)
-Given a start and an end index, creates a sub-array from the array passed as a parameter.
+Given a start and an end index, creates a sub-array from the array it is called on.
The element at the end index will not be included.
Also, all parameters are optional:
@@ -144,7 +144,8 @@ console.log(arr);
// => ['a', 'b', 'c', 'z']
```
-````exercism/caution
+
+~~~~exercism/caution
This default behavior leads to wrong results when you try to sort numbers.
```javascript
@@ -153,7 +154,8 @@ arr.sort();
// => [1, 10, 2, 3]
// Because the string '10' comes before '2' in dictionary order.
```
-````
+~~~~
+
To customize the sorting behavior, you can pass a comparison function as an argument.
The comparison function itself is called with two arguments which are two elements of the array.
diff --git a/concepts/array-transformations/introduction.md b/concepts/array-transformations/introduction.md
index ad879d6443..ab64b7628d 100644
--- a/concepts/array-transformations/introduction.md
+++ b/concepts/array-transformations/introduction.md
@@ -61,7 +61,7 @@ arr.reduce(
return accumulator;
},
- { even: [], odd: [] }
+ { even: [], odd: [] },
);
// => { even: [2, 4], odd: [1, 3] }
```
@@ -81,7 +81,7 @@ This method modifies the array it is called on.
### slice (pure)
-Given a start and an end index, creates a sub-array from the array passed as a parameter.
+Given a start and an end index, creates a sub-array from the array it is called on.
The element at the end index will not be included.
Also, all parameters are optional:
diff --git a/concepts/arrays/about.md b/concepts/arrays/about.md
index 4f6809093e..9efe4e1839 100644
--- a/concepts/arrays/about.md
+++ b/concepts/arrays/about.md
@@ -31,7 +31,7 @@ The array's object properties and list of array elements are separate, and the a
```javascript
const names = ['Jack', 'Laura', 'Paul', 'Megan'];
names.length;
-// => 3
+// => 4
// Properties can be set on arrays using bracket ['property'] or
// dot .property notation, and this will affect the length, as
@@ -210,9 +210,13 @@ names;
---
[^1]: `push`, MDN. (referenced September 29, 2021)
+
[^2]: `pop`, MDN. (referenced September 29, 2021)
+
[^3]: `shift`, MDN. (referenced September 29, 2021)
+
[^4]: `unshift`, MDN. (referenced September 29, 2021)
+
[^5]: `splice`, MDN. (referenced September 29, 2021)
[array-docs]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#Instance_methods
diff --git a/concepts/arrays/introduction.md b/concepts/arrays/introduction.md
index 6fd1e36393..8985e53b61 100644
--- a/concepts/arrays/introduction.md
+++ b/concepts/arrays/introduction.md
@@ -95,9 +95,13 @@ numbers;
---
[^1]: `push`, MDN. (referenced September 29, 2021)
+
[^2]: `pop`, MDN. (referenced September 29, 2021)
+
[^3]: `shift`, MDN. (referenced September 29, 2021)
+
[^4]: `unshift`, MDN. (referenced September 29, 2021)
+
[^5]: `splice`, MDN. (referenced September 29, 2021)
[array_methods]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array
diff --git a/concepts/basics/about.md b/concepts/basics/about.md
index 10c4496c76..e41c914c67 100644
--- a/concepts/basics/about.md
+++ b/concepts/basics/about.md
@@ -6,7 +6,7 @@ While it is most well-known as the scripting language for Web pages, many non-br
## (Re-)Assignment
-There are a few primary ways to assign values to names in JavaScript - using variables or constants. On Exercism, variables are always written in [camelCase][wiki-camel-case]; constants are written in [SCREAMING_SNAKE_CASE][wiki-snake-case]. There is no official guide to follow, and various companies and organizations have various style guides. _Feel free to write variables any way you like_. The upside from writing them the way the exercises are prepared is that they'll be highlighted differently in the web interface and most IDEs.
+There are a few primary ways to assign values to names in JavaScript - using variables or constants. On Exercism, variables are always written in [camelCase][wiki-camel-case]; constants are written in [SCREAMING_SNAKE_CASE][wiki-snake-case]. There is no official guide to follow, and various companies and organizations have various style guides. _Feel free to write variables any way you like_. The upside to writing them the way the exercises are prepared is that they'll be highlighted differently in the web interface and most IDEs.
Variables in JavaScript can be defined using the [`const`][mdn-const], [`let`][mdn-let] or [`var`][mdn-var] keyword.
diff --git a/concepts/basics/introduction.md b/concepts/basics/introduction.md
index 0e1fd62da2..a54e1c1e84 100644
--- a/concepts/basics/introduction.md
+++ b/concepts/basics/introduction.md
@@ -21,7 +21,7 @@ In contrast to `let` and `var`, variables that are defined with `const` can only
```javascript
const MY_FIRST_CONSTANT = 10;
-// Can not be re-assigned.
+// Cannot be re-assigned.
MY_FIRST_CONSTANT = 20;
// => TypeError: Assignment to constant variable.
```
diff --git a/concepts/callbacks/about.md b/concepts/callbacks/about.md
index 64fca0040f..1899983a55 100644
--- a/concepts/callbacks/about.md
+++ b/concepts/callbacks/about.md
@@ -81,7 +81,6 @@ You see this pattern often when dealing with asynchronous functions to assist wi
Common `Array` functions use callback functions to define their behaviour:
- `Array.prototype.forEach`:
-
- Accepts a callback, which applies the callback to each element of an array.
```javascript
@@ -92,7 +91,6 @@ Common `Array` functions use callback functions to define their behaviour:
```
- `Array.prototype.map`
-
- Accepts a callback, which applies the callback to each element of an array using the result to create a new array.
```javascript
@@ -103,7 +101,6 @@ Common `Array` functions use callback functions to define their behaviour:
```
- `Array.prototype.reduce`
-
- Accepts a callback, which applies the callback to each element of an array, passing the result forward to the next invocation.
```javascript
diff --git a/concepts/classes/about.md b/concepts/classes/about.md
index 0c47590f25..de48951a13 100644
--- a/concepts/classes/about.md
+++ b/concepts/classes/about.md
@@ -36,14 +36,16 @@ Before that, it was accessible via the key `__proto__` in many environments.
Do not confuse the prototype of an object (`[[prototype]]`) with the `prototype` property of the constructor function.
-```exercism/note
+
+~~~~exercism/note
To summarize:
- Constructors in JavaScript are regular functions.
- Constructing a new instance creates an object with a relation to its constructor called its _prototype_.
- Functions are objects (callable objects) and therefore they can have properties.
- The constructor's (function) `prototype` property will become the instance's _prototype_.
-```
+~~~~
+
### Instance Fields
@@ -124,11 +126,13 @@ The `[[prototype]]` of `Object` is usually `null` so the prototype chain ends th
In conclusion, you can call `myCar.toString()` and that method will exist because JavaScript searches for that method throughout the whole prototype chain.
You can find a detailed example in the [MDN article "Inheritance and the prototype chain"][mdn-prototype-chain-example].
-```exercism/caution
+
+~~~~exercism/caution
Note that the prototype chain is only travelled when retrieving a value.
Setting a property directly or deleting a property of an instance object only targets that specific instance.
This might not be what you would expect when you are used to a language with class-based inheritance.
-```
+~~~~
+
### Dynamic Methods (Adding Methods to All Existing Instances)
diff --git a/concepts/classes/introduction.md b/concepts/classes/introduction.md
index e6794ec62f..1f612f79f3 100644
--- a/concepts/classes/introduction.md
+++ b/concepts/classes/introduction.md
@@ -31,14 +31,16 @@ Every instance object includes a hidden, internal property referred to as `[[pro
It holds a reference to the value of the `prototype` key of the constructor function.
Yes, you read that correctly, a JavaScript function can have key/value pairs because it is also an object behind the scenes.
-```exercism/note
+
+~~~~exercism/note
To summarize:
- Constructors in JavaScript are regular functions.
- Constructing a new instance creates an object with a relation to its constructor called its _prototype_.
- Functions are objects (callable objects) and therefore they can have properties.
- The constructor's (function) `prototype` property will become the instance's _prototype_.
-```
+~~~~
+
### Instance Fields
@@ -117,11 +119,13 @@ The `[[prototype]]` property of `Car.prototype` (`myCar.[[prototype]].[[prototyp
It contains general methods that are available for all JavaScript objects, e.g. `toString()`.
In conclusion, you can call `myCar.toString()` and that method will exist because JavaScript searches for that method throughout the whole prototype chain.
-```exercism/caution
+
+~~~~exercism/caution
Note that the prototype chain is only travelled when retrieving a value.
Setting a property directly or deleting a property of an instance object only targets that specific instance.
This might not be what you would expect when you are used to a language with class-based inheritance.
-```
+~~~~
+
## Class Syntax
diff --git a/concepts/closures/about.md b/concepts/closures/about.md
index 957cac692a..23cf280315 100644
--- a/concepts/closures/about.md
+++ b/concepts/closures/about.md
@@ -18,7 +18,6 @@ The name _closure_ is historically derived from [_λ-calculus_][wiki-lambda-calc
## Reasons to use closures in JavaScript
1. Data Privacy / Data Encapsulation
-
- Unlike other languages, in 2020, there was no way to specify _private_ variables. So closures can be used to effectively emulate _private_ variables (there was a proposal to introduce private variable notation, which might have become standard by the time you read this).
```javascript
@@ -37,7 +36,6 @@ The name _closure_ is historically derived from [_λ-calculus_][wiki-lambda-calc
```
2. Partial Application
-
- Functions may return functions, and when a returned function uses the argument of the function that created it, this is an example of using a closure to perform partial application. Sometimes this is called _currying_ a function.
```javascript
diff --git a/concepts/closures/introduction.md b/concepts/closures/introduction.md
index c5104af10d..9d8409cb42 100644
--- a/concepts/closures/introduction.md
+++ b/concepts/closures/introduction.md
@@ -1,6 +1,7 @@
# Introduction
-**Closures** are a programming pattern in JavaScript that allows variables from an outer [lexical scope][wiki-lexical-scope] to be used inside of a nested block of code. JavaScript supports closures transparently, and they are often used without knowing what they are.
+**Closures** are a programming pattern in JavaScript which allows variables from an outer [lexical scope][wiki-lexical-scope] to be used inside of a nested block of code.
+JavaScript supports closures transparently, and they are often used without knowing what they are.
```javascript
// Top-level declarations are global-scope
@@ -12,16 +13,38 @@ const dozen = 12;
const twoDozen = dozen * 2;
}
+// Because of the block-scope declaration, twoDozen is not available here.
+twoDozen;
+// => Uncaught ReferenceError: twoDozen is not defined
+```
+
+Except for braces `{}`, functions (and classes) also create new scopes, which can _enclose_ values:
+
+```javascript
+const dozen = 12;
+
// Functions create a new function-scope and block-scope.
// Referencing the outer variable here is a closure.
function nDozen(n) {
- return dozen * n;
+ // This is declared inside the function scope, and uses the top-level scope.
+ // This works, and encloses the value 12.
+ const twoDozen = dozen * 2;
+
+ // This only uses the locally declared variable and the passed argument to the parameter `n`
+ return (twoDozen / 2) * n;
}
+
+// Because of the function-scope declaration, twoDozen is not available here.
+twoDozen;
+// => Uncaught ReferenceError: twoDozen is not defined
```
+As the `twoDozen` examples show, values can be enclosed in a _nested_ scope (function, block, etc.), but cannot be pulled out of that context.
+In the majority of cases, it is intended in Modern JavaScript that a value does not _leak_ to an outside scope.
+
## Closures to save state and pass along values
-Using a mutable variable declaration (like `let` or `var`) allows for state to be preserved:
+Using a mutable variable declaration (like `let` or `var`) allows for some state to be preserved:
```javascript
let counter = 0;
@@ -32,6 +55,67 @@ export function increment() {
counter += 1;
return counter;
}
+
+increment();
+// => 1
+
+counter;
+// => 1
+```
+
+## Enclosing values without leaking the state
+
+Combining the two ideas: enclosing a value to preserve state, and enclosed values do not leak to the outside, it's possible to create private values.
+
+The most common method is to make a function that returns a function which encloses some state.
+
+```javascript
+export function makeCounter() {
+ let counter = 0;
+
+ // This returns a new function that encloses the local variable counter
+ return function increment() {
+ counter += 1;
+ return counter;
+ };
+}
+
+// Counter did not leak
+counter;
+// => Uncaught ReferenceError: counter is not defined
+
+// This creates a new counter.
+// This assigns the increment function to the variable myFirstCounter.
+const myFirstCounter = makeCounter();
+
+typeof myFirstCounter;
+// => function
+
+myFirstCounter.name;
+// => increment
+
+myFirstCounter();
+// => 1
+myFirstCounter();
+// => 2
+
+// This creates new counter (with new, separate local state / enclosed counter variable)
+const mySecondCounter = makeCounter();
+
+mySecondCounter();
+// => 1
+
+// It is not affect the first counter.
+
+myFirstCounter();
+// => 3
+```
+
+```exercism/note
+Many programmers find closures a hard concept, and returning a function from a function is not common or not even possible in all programming languages.
+If you want more reading material, the [guide on MDN on Closures][mdn-closures] is quite comprehensive.
+
+[mdn-closures]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures
```
[wiki-lexical-scope]: https://en.wikipedia.org/wiki/Scope_(computer_science)#Lexical_scoping
diff --git a/concepts/dates/.meta/config.json b/concepts/dates/.meta/config.json
index 92048234c1..7f3da2f8e4 100644
--- a/concepts/dates/.meta/config.json
+++ b/concepts/dates/.meta/config.json
@@ -1,5 +1,5 @@
{
"blurb": "JavaScript has a built-in `Date` object which stores date and time and provides methods to work with them.",
- "authors": ["JaPatGitHub"],
+ "authors": ["SleeplessByte"],
"contributors": []
}
diff --git a/concepts/dates/about.md b/concepts/dates/about.md
index bd255c703d..454f948129 100644
--- a/concepts/dates/about.md
+++ b/concepts/dates/about.md
@@ -1,235 +1,181 @@
-# About
+# Introduction
JavaScript has a built-in object `Date` which stores date and time, and provides methods for their management.
+
+~~~exercism/caution
+It was based on Java's `java.util.Date` class, which was replaced in the early 2010s, but for backwards compatibility, JavaScript's `Date` sticks around.
+
+Because of how hard it is to work with Dates in general and because of how bad or non-existing timezone handling is, many libraries exist such as `moment.js`, `day.js`, `date-fns` and `luxon`.
+None of these are available on Exercism.
+
+In your own projects, do not use a deprecated / unmaintained package such as `moment.js` but rely on more modern alternatives like `luxon`, or the not yet widely available [Temporal][mdn-temporal].
+This exercise focusses on `Date`, which will remain relevant until the end of JavaScript.
+
+[mdn-temporal]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal
+~~~
+
## Creation
-A `Date` object in an instance of the `Date` class. It can be created without passing any arguments to the constructor function. This results in a `Date` object that represents the current date and time:
+A `Date` object in an instance of the `Date` class.
+It can be created without passing any arguments to the constructor function.
+This results in a `Date` object that represents the current date and time:
```javascript
const now = new Date();
// => Thu Apr 14 2022 11:46:08 GMT+0530 (India Standard Time)
-
-// Shows current day, date and time in your time zone.
-```
-
-However, different types of arguments can also be used to create date object, as follows:
-
-### Timestamp value
-
-> A timestamp is an integer number representing the number of **milliseconds** that has passed since **Jan 1st of 1970 [UTC][utc-defn]+0**, however, _with reference to your local time zone._
-> This can be used as an argument for the Date object.
->
-> ```javascript
-> const Jan01_1970 = new Date(0);
-> // 0 means 01.01.1970 UTC+0
->
-> const Jan02_1970 = new Date(24 * 3600 * 1000);
-> // adding 24 hours, we get 02.01.1970 UTC+0
->
-> // Note that the objects created here would show the corresponding time in your time zone.
-> ```
->
-> [^1]
-
-```exercism/note
-> January 1st, 1970 at 00:00:00 UTC is referred to as the Unix epoch.
-> Unix is an operating system originally developed in the 1960s.
-> Early Unix engineers picked that date arbitrarily because they needed to set a uniform date for the start of time, and > New Year's Day, 1970, seemed most convenient. [^2]
+// Shows current day, date and time (in your time zone).
```
-### Timestamp string
+### Unix timestamp (number)
-You can pass a string value representing a date to the `Date` constructor.
-The string needs to follow a format that is recognized by the `Date.parse()` method.
-You will learn more about this below.
-
-### Date object
-
-An existing date object can also be used as an argument.
-This makes a copy of the existing `Date` object with the same date and time.
+If a number is passed in, this will be interpreted as a `timestamp`.
+A timestamp is an integer number representing the number of **milliseconds** that has passed since **1 January 1970 [UTC][defn-utc]+0**.
```javascript
-const t1 = new Date();
-const t2 = new Date(t1);
+const epoch = new Date(0);
+// Thu Jan 01 1970 01:00:00 GMT+0100 (Central European Standard Time)
-// Values of t1 and t2 will be the same.
+const another = new Date(1749508766627);
+// Tue Jun 10 2025 00:39:26 GMT+0200 (Central European Summer Time)
```
-### Individual date and time component values
-
-> Given at least a year and month, this form of `Date()` returns a `Date` object whose component values _(year, month, day, hour, minute, second, and millisecond)_ all come from the following parameters.
-> Any missing fields are given the lowest possible value (1 for day and 0 for every other component).
-> The parameter values are all evaluated against the _local time zone, rather than UTC_.
->
-> - `year`: Integer values from 0 to 99 map to the years 1900 to 1999.
-> All other values are the actual year.
-> - `monthIndex`: Integer value representing the month, beginning with _0 for January to 11 for December_.
-> If a value greater than 11 is passed in, then those months will be added to the date.
-> For example, new Date(1990, 12, 1) will return January 1st, 1991.
-> - `day` (Optional): Integer value representing the day of the month.
-> The default is 1.
-> - `hours` (Optional): Integer value between 0 and 23 representing the hour of the day.
-> Defaults to 0.
-> - `minutes` (Optional): Integer value representing the minute segment of a time.
-> The default is 0 minutes past the hour.
-> - `seconds` (Optional): Integer value representing the second segment of a time.
-> The default is 0 seconds past the minute.
-> - `milliseconds` (Optional): Integer value representing the millisecond segment of a time.
-> The default is 0 milliseconds past the second.
->
-> [^3]
+One may expect `new Date(0)` to generate the "earliest" date object, but JavaScript will convert the date to your local timezone, which means that only those around [GMT / with an UTC+0][defn-gmt] timezone will actually get the [Unix epoch][defn-unix-epoch] value.
-```javascript
-const date1 = new Date(95, 11, 17);
-// Creates Date for Dec 17 1995 00:00 if your local timezone is equivalent to UTC.
+### ISO 8601 timestamp (string)
-const date2 = new Date(2013, 12, 5, 13, 24, 0);
-// Creates Date for Jan 5 2014 13:24 if your local timezone is equivalent to UTC.
-```
+You can pass a string value representing a date to the `Date` constructor.
+The **only** format that is consistent across implementations is the [simplified version][mdn-date-string-format] of the internationally recognized and standardized so-called [ISO 8601 timestamp strings][defn-iso8601].
-## `Date.parse()`
+A moment in time at [UTC][defn-gmt] looks like this:
-`Date.parse()` takes **string as a input and returns a timestamp** (number of milliseconds from 1 Jan 1970 UTC+0), provided the string is in the format YYYY-MM-DDTHH:mm:ss.sssZ, where:
+```text
+YYYY-MM-DDTHH:mm:ss.mssZ
+YYYYMMDDTHHmmss.mssZ
+```
-> - `YYYY-MM-DD` - is the date: year-month-day.
-> - `T` - The character "T" is used as the delimiter
-> - `HH:mm:ss.sss` - is the time: hours, minutes, seconds and milliseconds.
-> - `Z` - This _optional_ part denotes the time zone.
-> If `Z` is present, the `Date` will be set to UTC.
-> If `Z` is not present, it will be Local Time.
->
-> If the format is invalid, `NaN` is returned. [^4]
+Where the following substitutions take place:
-Shorter variants are also possible, like `YYYY-MM-DD` or `YYYY-MM` or even `YYYY`. However, note that these variants **set the `Date` to UTC**, even though `Z` not mentioned.
-To understand what exactly happens check out [this section][mdn-diff-assumed-timezone] of a MDN page.
+| Key | Description | Default |
+| ---- | --------------------------------------------- | ------- |
+| YYYY | The calendar year, represented in 4 digits | |
+| MM | The calendar month, represented in 2 digits | 01 |
+| DD | The calendar day, represented in 2 digits | 01 |
+| T | A literal letter T, separating date from time | |
+| HH | The hours in a 24-hour clock, 2 digits | 00 |
+| mm | The minutes, 2 digits | 00 |
+| ss | The seconds, 2 digits | 00 |
+| mss | The milliseconds, 3 digits | 000 |
+| Z | A literal letter Z, or an offset `+/-HH:mm` | |
-```javascript
-const d1 = Date.parse('2019-01-01');
-const d2 = Date.parse('2019-01-01T00:00:00.000Z');
+The literal letter `Z` indicates UTC (no timezone, no Day Light Savings).
-// Both d1 and d2 are of value 1546300800000, as times are set to UTC.
+Because there are default values for most components, leaving parts off at the end is valid:
-const d3 = Date.parse('2019-01-01T00:00:00.000');
-// This would have a different value (unless you live in GMT) as
-// it is set to your local time zone.
+```text
+YYYY-MM-DD
```
-```exercism/caution
-The use of `Date.parse()` (and the timestamp string method which works similarly) is strongly discouraged due to browser differences and inconsistencies. [^5]
-```
+Defaults to a time of 00:00:00.000
-## Accessing `Date` components
+If the timestamp does not end in `Z`, and it does not end with `+HH:mm` or `-HH:mm` (indicating a timezone offset), because of historical reasons, the following applies:
-The following are the methods to access the year, month and so on from the Date object:
+> When the time zone offset is absent, date-only forms are interpreted as a UTC time and date-time forms are interpreted as a local time.
+> The interpretation as a UTC time is due to a historical spec error that was not consistent with ISO 8601 but could not be changed due to web compatibility.
+> See [Broken Parser – A Web Reality Issue][ref-broken-parser].
-> - `getFullYear()`- Get the year (4 digits)
-> - `getMonth()`- Get the month, from 0 to 11.
-> - `getDate()`- Get the day of month, from 1 to 31.
-> - `getHours()`, `getMinutes()`, `getSeconds()`, `getMilliseconds()`- Get the corresponding time components.
-> - `getDay()`- Get the day of week, from 0 (Sunday) to 6 (Saturday).
-> - `getTime()`- Get the number of milliseconds passed since 01.01.1970 UTC.
->
-> [^6]
+### Date object
-```javascript
-const date0 = new Date(0); //Jan 1 1970 00:00:00
-let month = date0.getMonth()); // => 0; as Jan is the month
-let date = date0.getDay(); // Find out which day the new year of 1970 was!
+An existing date object can also be used as a constructor argument.
+This makes a copy of the existing `Date` object with the same date and time.
-const date1 = new Date(2020, 11, 13, 5); // Dec 13 2020 5:00:00
-let millsecs = date1.getTime(); // find out how many have milliseconds passed since Jan 1 1890!
+```javascript
+const t1 = new Date();
+const t2 = new Date(t1);
+// Values of t1 and t2 will be the same.
```
-```exercism/caution
-Many JavaScript engines implement a non-standard method `getYear()`.
-**This method is deprecated.**
-It returns a 2-digit year sometimes.
-Hence, `getFullYear()` must always be used instead.
-```
+### Supplying individual date and time component values
-## Modifying `Date` components
+A date representing a date can be created by passing three numbers.
+A date representing a date and time can be created by passing in 6 numbers.
-The following methods allow to modify date/time components :
+```javascript
+const date1 = new Date(95, 11, 17);
+// Creates Date for Dec 17 1995 00:00 if your local timezone is equivalent to UTC.
-> - `setFullYear(year, [month], [date])`
-> - `setMonth(month, [date])`
-> - `setDate(date)`
-> - `setHours(hour, [min], [sec], [ms])`
-> - `setMinutes(min, [sec], [ms])`
-> - `setSeconds(sec, [ms])`
-> - `setMilliseconds(ms)`
-> - `setTime(timestamp)` (sets the whole date by milliseconds since 01.01.1970 UTC)
->
-> Parameters in `[]` above are _optional_.
-> If not mentioned, the components are not modified.
-> Every one of them except `setTime()` has a UTC-variant, for instance: `setUTCHours()`. [^7]
+const date2 = new Date(2013, 12, 5, 13, 24, 0);
+// Creates Date for Jan 5 2014 13:24 if your local timezone is equivalent to UTC.
+```
-```javascript
-let today = new Date();
+The second value is the `month`, which starts at `0` for January, up to `11` for December.
-today.setHours(0); // still today, but only the hour is changed to 0
+## `Date.parse()`
-today.setHours(0, 0, 0, 0); // still today, now sharply 00:00:00
-```
+You may find mentions of or references to a date parsing function `Date.parse`.
+Because there are only a few invariants (truths) for this function and because the rest of the implementation is not specified (and thus not standard), one should not use it.
-## Calculating Time Difference and `Date.now()`
+## Accessing `Date` components
-To measure the time elapsed between two given dates, we can use the `Date.getTime()` method.
+There are various methods on date objects that return the components of the date:
```javascript
-const d1 = new Date(2021, 12, 11, 5, 13, 32, 21);
-const d2 = new Date(2021, 12, 23, 4, 12, 55);
-
-let timeElapsed = d2.getTime() - d1.getTime(); // => 1033162979
+getFullYear(); // Get the year (4 digits)
+getMonth(); // Get the month, from 0 to 11.
+getDate(); // Get the day of month, from 1 to 31.
+getHours(); // Get the hour in a 24 clock, from 0 to 23
+getMinutes(); // Get the minutes, from 0 to 59
+getSeconds(); // Get the seconds, from 0 to 59
+getMilliseconds(); // Get the milliseconds, from 0 and 999
+getDay(); // Get the day of week, from 0 (Sunday) to 6 (Saturday).
```
-Moreover, if we wish to measure the time taken on a live basis, for example the time taken for execution for program, we could use `Date.now()` which provides the timestamp of current time.
-
-> As you might notice, this is semantically equivalent to `new Date().getTime()`, but it doesn’t create an intermediate `Date` object.
-> Hence, it makes the code more efficient. [^8]
+Each of these has an applicable `set` variant (e.g. `setFullYear`) to update the value.
+Any overflowing value rolls over to the next component:
```javascript
-const start = Date.now(); // milliseconds count from 1 Jan 1970
+const date = new Date('2025-02-28T12:42:00Z');
+// => Fri Feb 28 2025 13:42:00 GMT+0100 (Central European Standard Time)
-// execute a task
-for (let i = 0; i < 100000; i++) {
- let task = i * i * i * i;
-}
+date.setDate(29);
+// there was no February 29th in 2025.
-const end = Date.now(); // done
+date.getDate();
+// => 1
-let duration = end - start;
-// how long it took to run the loop, in seconds
+date.toString();
+// => Sat Mar 01 2025 13:42:00 GMT+0100 (Central European Standard Time)
```
-## Comparing Dates
+There are UTC variants for all the methods that disregard the local timezone.
-We can use `<` and `>` operators to compare two `Date` objects, the date occuring _later being treated as greater_.
+## Converting from date
-The `==` or `===` do not work with `Date`, and output `false` in any case, even if dates are equal.
-However, we could use the `Date.getTime()` method to obtain the timestamps (which is of the data type `number`) and compare them using equality operators.
+Date objects have a method `getTime()` that returns the UNIX timestamp in milliseconds, ie. amount of milliseconds the midnight at the beginning of January 1, 1970, UTC.
+Additionally, a method `toISOString()` is available to convert from a date object to a ISO 8601 timestamp string.
-```javascript
-const d1 = new Date(2021, 12, 11);
-const d2 = new Date(1990, 11, 23);
+## Comparing Dates
-d1 > d2; // true
+Greater than (`>`) and greater than or equals (`>=`) as well as less than (`<`) and less than or equals (`<=`) can be used directly between two dates or a date and a number.
+This works because JavaScript will try to coerce the date to a primitive.
-const d1Copy = new Date(d1); // d1Copy will be same as d1
+
+~~~exercism/advanced
+When doing a comparison between two dates or date and a number, JavaScript calls [`[Symbol.toPrimitive]("number")`][mdn-to-primitive] which internally calls [`date.valueOf()`][mdn-date-value-of].
+The latter is the same as calling [`date.getTime()`][mdn-date-get-time].
-d1Copy === d1; // false, even though they are same
-d1Copy.getTime() === d1.getTime(); //true
-```
+If you do not want to rely on this behaviour, convert to a number using `getTime()` first.
+
+[mdn-to-primitive]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Symbol.toPrimitive
+[mdn-date-value-of]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/valueOf
+[mdn-date-get-time]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getTime
+~~~
+
+Dates cannot be compared using equality (`==`, and `===`), but the result of `.getTime()` can.
-[^1]: https://javascript.info/date
-[^2]: https://kb.narrative.io/what-is-unix-time
-[^3]: https://javascript.info/date#setting-date-components
-[^4]: https://javascript.info/date#date-parse-from-a-string
-[^5]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date#several_ways_to_create_a_date_object
-[^6]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date#several_ways_to_create_a_date_object
-[^7]: https://javascript.info/date#access-date-components
-[^8]: https://javascript.info/date#date-now
-
-[utc-defn]: https://simple.wikipedia.org/wiki/Coordinated_Universal_Time
-[mdn-diff-assumed-timezone]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse#differences_in_assumed_time_zone
+[defn-utc]: https://simple.wikipedia.org/wiki/Coordinated_Universal_Time
+[defn-gmt]: https://simple.wikipedia.org/wiki/Greenwich_Mean_Time
+[defn-unix-epoch]: https://en.wikipedia.org/wiki/Epoch_%28computing%29
+[defn-iso8601]: https://en.wikipedia.org/wiki/ISO_8601
+[mdn-date-string-format]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format
+[ref-broken-parser]: https://maggiepint.com/2017/04/11/fixing-javascript-date-web-compatibility-and-reality/
diff --git a/concepts/dates/introduction.md b/concepts/dates/introduction.md
index d536746f41..0c307fbe5b 100644
--- a/concepts/dates/introduction.md
+++ b/concepts/dates/introduction.md
@@ -2,77 +2,103 @@
JavaScript has a built-in object `Date` which stores date and time, and provides methods for their management.
+
+~~~exercism/caution
+It was based on Java's `java.util.Date` class, which was replaced in the early 2010s, but for backwards compatibility, JavaScript's `Date` sticks around.
+
+Because of how hard it is to work with Dates in general and because of how bad or non-existing timezone handling is, many libraries exist such as `moment.js`, `day.js`, `date-fns` and `luxon`.
+None of these are available on Exercism.
+
+In your own projects, do not use a deprecated / unmaintained package such as `moment.js` but rely on more modern alternatives like `luxon`, or the not yet widely available [Temporal][mdn-temporal].
+This exercise focusses on `Date`, which will remain relevant until the end of JavaScript.
+
+[mdn-temporal]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal
+~~~
+
## Creation
-A `Date` object in an instance of the `Date` class. It can be created without passing any arguments to the constructor function. This results in a `Date` object that represents the current date and time:
+A `Date` object in an instance of the `Date` class.
+It can be created without passing any arguments to the constructor function.
+This results in a `Date` object that represents the current date and time:
```javascript
const now = new Date();
// => Thu Apr 14 2022 11:46:08 GMT+0530 (India Standard Time)
-
-// Shows current day, date and time in your time zone.
+// Shows current day, date and time (in your time zone).
```
-However, different types of arguments can also be used to create date object, as follows:
+### Unix timestamp (number)
-### Timestamp value
+If a number is passed in, this will be interpreted as a `timestamp`.
+A timestamp is an integer number representing the number of **milliseconds** that has passed since **1 January 1970 [UTC][defn-utc]+0**.
-> A timestamp is an integer number representing the number of **milliseconds** that has passed since **Jan 1st of 1970 [UTC][utc-defn]+0**, however, _with reference to your local time zone._
-> This can be used as an argument for the Date object.
->
-> ```javascript
-> const Jan01_1970 = new Date(0);
-> // 0 means 01.01.1970 UTC+0
->
-> const Jan02_1970 = new Date(24 * 3600 * 1000);
-> // adding 24 hours, we get 02.01.1970 UTC+0
->
-> // Note that the objects created here would show the corresponding time in your time zone.
-> ```
->
-> [^1]
+```javascript
+const epoch = new Date(0);
+// Thu Jan 01 1970 01:00:00 GMT+0100 (Central European Standard Time)
-### Timestamp string
+const another = new Date(1749508766627);
+// Tue Jun 10 2025 00:39:26 GMT+0200 (Central European Summer Time)
+```
+
+One may expect `new Date(0)` to generate the "earliest" date object, but JavaScript will convert the date to your local timezone, which means that only those around [GMT / with an UTC+0][defn-gmt] timezone will actually get the [Unix epoch][defn-unix-epoch] value.
+
+### ISO 8601 timestamp (string)
You can pass a string value representing a date to the `Date` constructor.
-The string needs to follow a format that is recognized by the `Date.parse()` method.
-You will learn more about this below.
+The **only** format that is consistent across implementations is the [simplified version][mdn-date-string-format] of the internationally recognized and standardized so-called [ISO 8601 timestamp strings][defn-iso8601].
+
+A moment in time at [UTC][defn-gmt] looks like this:
+
+```text
+YYYY-MM-DDTHH:mm:ss.mssZ
+YYYYMMDDTHHmmss.mssZ
+```
+
+Where the following substitutions take place:
+
+| Key | Description | Default |
+| ---- | ------------------------------------------- | ------- |
+| YYYY | The calendar year, represented in 4 digits | |
+| MM | The calendar month, represented in 2 digits | 01 |
+| DD | The calendar day, represented in 2 digits | 01 |
+| T | A literal letter T, separating date & time | T |
+| HH | The hours in a 24-hour clock, 2 digits | 00 |
+| mm | The minutes, 2 digits | 00 |
+| ss | The seconds, 2 digits | 00 |
+| mss | The milliseconds, 3 digits | 000 |
+| Z | A literal letter Z, or an offset `+/-HH:mm` | |
+
+The literal letter `Z` indicates UTC (no timezone, no Day Light Savings).
+
+Because there are default values for most components, leaving parts off at the end is valid:
+
+```text
+YYYY-MM-DD
+```
+
+Defaults to a time of 00:00:00.000
+
+If the timestamp does not end in `Z`, and it does not end with `+HH:mm` or `-HH:mm` (indicating a timezone offset), because of historical reasons, the following applies:
+
+> When the time zone offset is absent, date-only forms are interpreted as a UTC time and date-time forms are interpreted as a local time.
+> The interpretation as a UTC time is due to a historical spec error that was not consistent with ISO 8601 but could not be changed due to web compatibility.
+> See [Broken Parser – A Web Reality Issue][ref-broken-parser].
### Date object
-An existing date object can also be used as an argument.
+An existing date object can also be used as a constructor argument.
This makes a copy of the existing `Date` object with the same date and time.
```javascript
const t1 = new Date();
const t2 = new Date(t1);
-
// Values of t1 and t2 will be the same.
```
-### Individual date and time component values
-
-> Given at least a year and month, this form of `Date()` returns a `Date` object whose component values _(year, month, day, hour, minute, second, and millisecond)_ all come from the following parameters.
-> Any missing fields are given the lowest possible value (1 for day and 0 for every other component).
-> The parameter values are all evaluated against the _local time zone, rather than UTC_.
->
-> - `year`: Integer values from 0 to 99 map to the years 1900 to 1999.
-> All other values are the actual year.
-> - `monthIndex`: Integer value representing the month, beginning with _0 for January to 11 for December_.
-> If a value greater than 11 is passed in, then those months will be added to the date.
-> For example, new Date(1990, 12, 1) will return January 1st, 1991.
-> - `day` (Optional): Integer value representing the day of the month.
-> The default is 1.
-> - `hours` (Optional): Integer value between 0 and 23 representing the hour of the day.
-> Defaults to 0.
-> - `minutes` (Optional): Integer value representing the minute segment of a time.
-> The default is 0 minutes past the hour.
-> - `seconds` (Optional): Integer value representing the second segment of a time.
-> The default is 0 seconds past the minute.
-> - `milliseconds` (Optional): Integer value representing the millisecond segment of a time.
-> The default is 0 milliseconds past the second.
->
-> [^2]
+### Supplying individual date and time component values
+
+A date representing a date can be created by passing three numbers.
+A date representing a date and time can be created by passing in 6 numbers.
```javascript
const date1 = new Date(95, 11, 17);
@@ -82,111 +108,74 @@ const date2 = new Date(2013, 12, 5, 13, 24, 0);
// Creates Date for Jan 5 2014 13:24 if your local timezone is equivalent to UTC.
```
-## `Date.parse()`
-
-`Date.parse()` takes **string as a input and returns a timestamp** (number of milliseconds from 1 Jan 1970 UTC+0), provided the string is in the format YYYY-MM-DDTHH:mm:ss.sssZ, where:
-
-> - `YYYY-MM-DD` - is the date: year-month-day.
-> - `T` - The character "T" is used as the delimiter
-> - `HH:mm:ss.sss` - is the time: hours, minutes, seconds and milliseconds.
-> - `Z` - This _optional_ part denotes the time zone.
-> If `Z` is present, the `Date` will be set to UTC.
-> If `Z` is not present, it will be Local Time.
->
-> If the format is invalid, `NaN` is returned. [^3]
+The second value is the `month`, which starts at `0` for January, up to `11` for December.
-Shorter variants are also possible, like `YYYY-MM-DD` or `YYYY-MM` or even `YYYY`. However, note that these variants **set the `Date` to UTC**, even though `Z` not mentioned.
-To understand what exactly happens check out [this section][mdn-diff-assumed-timezone] of a MDN page.
+## `Date.parse()`
-```exercism/caution
-The use of `Date.parse()` (and the timestamp string method which works similarly) is strongly discouraged due to browser differences and inconsistencies. [^4]
-```
+You may find mentions of or references to a date parsing function `Date.parse`.
+Because there are only a few invariants (truths) for this function and because the rest of the implementation is not specified (and thus not standard), one should not use it.
## Accessing `Date` components
-The following are the methods to access the year, month and so on from the Date object:
-
-> - `getFullYear()`- Get the year (4 digits)
-> - `getMonth()`- Get the month, from 0 to 11.
-> - `getDate()`- Get the day of month, from 1 to 31.
-> - `getHours()`, `getMinutes()`, `getSeconds()`, `getMilliseconds()`- Get the corresponding time components.
-> - `getDay()`- Get the day of week, from 0 (Sunday) to 6 (Saturday).
-> - `getTime()`- Get the number of milliseconds passed since 01.01.1970 UTC.
->
-> [^5]
+There are various methods on date objects that return the components of the date:
```javascript
-const date0 = new Date(0); //Jan 1 1970 00:00:00
-let month = date0.getMonth()); // => 0; as Jan is the month
-let date = date0.getDay(); // Find out which day the new year of 1970 was!
-
-const date1 = new Date(2020, 11, 13, 5); // Dec 13 2020 5:00:00
-let millsecs = date1.getTime(); // find out how many have milliseconds passed since Jan 1 1890!
+getFullYear(); // Get the year (4 digits)
+getMonth(); // Get the month, from 0 to 11.
+getDate(); // Get the day of month, from 1 to 31.
+getHours(); // Get the hour in a 24 clock, from 0 to 23
+getMinutes(); // Get the minutes, from 0 to 59
+getSeconds(); // Get the seconds, from 0 to 59
+getMilliseconds(); // Get the milliseconds, from 0 and 999
+getDay(); // Get the day of week, from 0 (Sunday) to 6 (Saturday).
```
-## Modifying `Date` components
-
-The following methods allow to modify date/time components :
-
-> - `setFullYear(year, [month], [date])`
-> - `setMonth(month, [date])`
-> - `setDate(date)`
-> - `setHours(hour, [min], [sec], [ms])`
-> - `setMinutes(min, [sec], [ms])`
-> - `setSeconds(sec, [ms])`
-> - `setMilliseconds(ms)`
-> - `setTime(timestamp)` (sets the whole date by milliseconds since 01.01.1970 UTC)
->
-> Parameters in `[]` above are _optional_.
-> If not mentioned, the components are not modified.
-> Every one of them except `setTime()` has a UTC-variant, for instance: `setUTCHours()`. [^6]
+Each of these has an applicable `set` variant (e.g. `setFullYear`) to update the value.
+Any overflowing value rolls over to the next component:
```javascript
-let today = new Date();
-
-today.setHours(0); // still today, but only the hour is changed to 0
-
-today.setHours(0, 0, 0, 0); // still today, now sharply 00:00:00
-```
-
-## Calculating Time Difference and `Date.now()`
+const date = new Date('2025-02-28T12:42:00Z');
+// => Fri Feb 28 2025 13:42:00 GMT+0100 (Central European Standard Time)
-To measure the time elapsed between two given dates, we can use the `Date.getTime()` method.
+date.setDate(29);
+// there was no February 29th in 2025.
-```javascript
-const d1 = new Date(2021, 12, 11, 5, 13, 32, 21);
-const d2 = new Date(2021, 12, 23, 4, 12, 55);
+date.getDate();
+// => 1
-let timeElapsed = d2.getTime() - d1.getTime(); // => 1033162979
+date.toString();
+// => Sat Mar 01 2025 13:42:00 GMT+0100 (Central European Standard Time)
```
-Moreover, if we wish to measure the time taken on a live basis, for example the time taken for execution for program, we could use `Date.now()` which provides the timestamp of current time.
+There are UTC variants for all the methods that disregard the local timezone.
-## Comparing Dates
+## Converting from date
-We can use `<` and `>` operators to compare two `Date` objects, the date occuring _later being treated as greater_.
+Date objects have a method `getTime()` that returns the UNIX timestamp in milliseconds, ie. amount of milliseconds the midnight at the beginning of January 1, 1970, UTC.
+Additionally, a method `toISOString()` is available to convert from a date object to a ISO 8601 timestamp string.
-The `==` or `===` do not work with `Date`, and output `false` in any case, even if dates are equal.
-However, we could use the `Date.getTime()` method to obtain the timestamps (which is of the data type `number`) and compare them using equality operators.
+## Comparing Dates
-```javascript
-const d1 = new Date(2021, 12, 11);
-const d2 = new Date(1990, 11, 23);
+Greater than (`>`) and greater than or equals (`>=`) as well as less than (`<`) and less than or equals (`<=`) can be used directly between two dates or a date and a number.
+This works because JavaScript will try to coerce the date to a primitive.
-d1 > d2; // true
+
+~~~exercism/advanced
+When doing a comparison between two dates or date and a number, JavaScript calls [`[Symbol.toPrimitive]("number")`][mdn-to-primitive] which internally calls [`date.valueOf()`][mdn-date-value-of].
+The latter is the same as calling [`date.getTime()`][mdn-date-get-time].
-const d1Copy = new Date(d1); // d1Copy will be same as d1
+If you do not want to rely on this behaviour, convert to a number using `getTime()` first.
-d1Copy === d1; // false, even though they are same
-d1Copy.getTime() === d1.getTime(); //true
-```
+[mdn-to-primitive]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Symbol.toPrimitive
+[mdn-date-value-of]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/valueOf
+[mdn-date-get-time]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getTime
+~~~
-[^1]: https://javascript.info/date
-[^2]: https://javascript.info/date#setting-date-components
-[^3]: https://javascript.info/date#date-parse-from-a-string
-[^4]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date#several_ways_to_create_a_date_object
-[^5]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date#several_ways_to_create_a_date_object
-[^6]: https://javascript.info/date#access-date-components
+Dates cannot be compared using equality (`==`, and `===`), but the result of `.getTime()` can.
-[utc-defn]: https://simple.wikipedia.org/wiki/Coordinated_Universal_Time
-[mdn-diff-assumed-timezone]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse#differences_in_assumed_time_zone
+[defn-utc]: https://simple.wikipedia.org/wiki/Coordinated_Universal_Time
+[defn-gmt]: https://simple.wikipedia.org/wiki/Greenwich_Mean_Time
+[defn-unix-epoch]: https://en.wikipedia.org/wiki/Epoch_%28computing%29
+[defn-iso8601]: https://en.wikipedia.org/wiki/ISO_8601
+[mdn-date-string-format]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format
+[ref-broken-parser]: https://maggiepint.com/2017/04/11/fixing-javascript-date-web-compatibility-and-reality/
diff --git a/concepts/dates/links.json b/concepts/dates/links.json
index 6b22e9127f..115304b685 100644
--- a/concepts/dates/links.json
+++ b/concepts/dates/links.json
@@ -4,11 +4,23 @@
"description": "javascript.info: Date"
},
{
- "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date",
- "description": "MDN: Date() Constructor"
+ "url": "https://en.wikipedia.org/wiki/Epoch_%28computing%29",
+ "description": "Wikipedia: epoch (computing)"
},
{
- "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse",
- "description": "MDN: Date.parse()"
+ "url": "https://en.wikipedia.org/wiki/ISO_8601",
+ "description": "Wikipedia: ISO 8601 format"
+ },
+ {
+ "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date",
+ "description": "MDN: Date"
+ },
+ {
+ "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format",
+ "description": "MDN: Date time string format"
+ },
+ {
+ "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal",
+ "description": "MDN: Temporal"
}
]
diff --git a/concepts/errors/introduction.md b/concepts/errors/introduction.md
index 138afd6610..39fcd13cd9 100644
--- a/concepts/errors/introduction.md
+++ b/concepts/errors/introduction.md
@@ -2,7 +2,7 @@
Errors are useful to report when something is wrong or unexpected in a program or a piece of code.
-They are javascript objects.
+They are JavaScript objects.
The main property of this object is `message`:
diff --git a/concepts/null-undefined/introduction.md b/concepts/null-undefined/introduction.md
index 99d2025ba4..f1cb19446e 100644
--- a/concepts/null-undefined/introduction.md
+++ b/concepts/null-undefined/introduction.md
@@ -31,15 +31,38 @@ That means while `null` represents an empty value (but still a value), `undefine
`undefined` appears in different contexts.
- If a variable is declared without a value (initialization), it is `undefined`.
+
+ ```js
+ let name;
+ console.log(name); // => undefined
+ ```
+
- If you try to access a value for a non-existing key in an object, you get `undefined`.
+
+ ```js
+ let obj = {
+ name: 'John',
+ };
+ console.log(obj.age); // => undefined
+ ```
+
- If a function does not return a value, the result is `undefined`.
+
+ ```js
+ function printName(name) {
+ 'My name is ' + name;
+ }
+ console.log(printName('John')); // => undefined
+ ```
+
- If an argument is not passed to a function, it is `undefined`, unless that argument has a default value.
-```javascript
-let name;
-console.log(name);
-// => undefined
-```
+ ```js
+ function printName(name) {
+ return name;
+ }
+ console.log(printName()); // => undefined
+ ```
You can check whether a variable is undefined using the strict equality operator `===`.
diff --git a/concepts/numbers/about.md b/concepts/numbers/about.md
index cefa271b7b..b9d13b5d75 100644
--- a/concepts/numbers/about.md
+++ b/concepts/numbers/about.md
@@ -26,7 +26,7 @@ Numbers may also be expressed in literal forms like `0b101`, `0o13`, `0x0A`. Lea
### Exponential Notation
The E-notation indicates a number that should be multiplied by 10 raised to a given power.
-The format of E-notation is to have a number, followed by `e` or `E`, than by the power of 10 to multiply by.
+The format of E-notation is to have a number, followed by `e` or `E`, then by the power of 10 to multiply by.
```javascript
const num = 3.125e7;
@@ -37,7 +37,7 @@ const num = 3.125e7;
E-notation can also be used to represent very small numbers:
```javascript
-const num = 325987e-6; // Equals to 0. 325987
+const num = 325987e-6; // Equals 0.325987
// The notation essentially says, "Take 325987 and multiply it by 10^-6.
```
@@ -153,7 +153,7 @@ isFinite(NaN); // => false
`+0` or `-0` are distinct numbers in JavaScript. They can be produced if you represented a number, that is so small that it is indistinguishable from 0.
The signed zero allows you to record “from which direction” you approached zero; that is, what sign the number had before it was considered zero.
-It is best practise to pretend there's only one zero.
+It is best practice to pretend there's only one zero.
## Comparison
diff --git a/concepts/numbers/introduction.md b/concepts/numbers/introduction.md
index 4327074891..6cef5b1aab 100644
--- a/concepts/numbers/introduction.md
+++ b/concepts/numbers/introduction.md
@@ -7,7 +7,7 @@ Many programming languages have specific numeric types to represent different ty
- `bigint`: a numeric data type that can represent _integers_ in the arbitrary precision format.
Examples are `-12n`, `0n`, `4n`, and `9007199254740991n`.
-If you require arbitrary precision or work with extremely large numbers, use the `bigint` type.
+If you require [arbitrary precision][ref-arbitrary-precision] or work with extremely large numbers, use the `bigint` type.
Otherwise, the `number` type is likely the better option.
## Rounding
@@ -20,3 +20,4 @@ Math.ceil(234.34); // => 235
```
[ref-math-object-rounding]: https://javascript.info/number#rounding
+[ref-arbitrary-precision]: https://en.wikipedia.org/wiki/Arbitrary-precision_arithmetic
diff --git a/concepts/object-destructuring/.meta/config.json b/concepts/object-destructuring/.meta/config.json
new file mode 100644
index 0000000000..3500286e76
--- /dev/null
+++ b/concepts/object-destructuring/.meta/config.json
@@ -0,0 +1,5 @@
+{
+ "blurb": "Object destructuring is a concise way of extracting properties from an object.",
+ "authors": ["SleeplessByte"],
+ "contributors": []
+}
diff --git a/concepts/object-destructuring/about.md b/concepts/object-destructuring/about.md
new file mode 100644
index 0000000000..b4fd7ce627
--- /dev/null
+++ b/concepts/object-destructuring/about.md
@@ -0,0 +1,129 @@
+# About
+
+Object [destructuring][mdn-object-destructuring] is a concise way of extracting properties from an object.
+Its syntax is similar to an [object literal][mdn-object-literal] expression, but on the left-hand side of the assignment instead of the right.
+
+```javascript
+const weather = {
+ sun: '☀️',
+ sun_behind_small_cloud: '🌤️',
+ sun_behind_cloud: '⛅',
+ sun_behind_large_cloud: '🌥️',
+ sun_behind_rain_cloud: '🌦️',
+ cloud: '☁️',
+ cloud_with_rain: '🌧️',
+ cloud_with_snow: '🌨️',
+ cloud_with_lightning: '🌩️',
+ cloud_with_lightning_and_rain: '⛈️',
+};
+
+const { sun, cloud, cloud_with_lightning } = weather;
+
+sun;
+// => '☀️'
+
+cloud;
+// => '☁️'
+
+cloud_with_lightning;
+// => '🌩️'
+```
+
+## Renaming in assignment
+
+The syntax can extract the properties by their key like `sun`, `cloud`, and `cloud_with_lightning`, but can also pick a different name for the variable:
+
+```javascript
+const { sun: sunny, cloud: cloudy, cloud_with_rain: rainy } = weather;
+
+typeof cloud_with_rain;
+// => 'undefined'
+
+typeof rainy;
+// => 'string'
+
+rainy;
+// => 🌧️
+```
+
+The assignment is also not required to use all the values.
+
+## Default values
+
+The object destructuring assignment can provide _default values_ in case there is none in the source object:
+
+```javascript
+const { sun = '🌞', tornado = '🌪️', cloud_with_snow: snowy = '❄️' } = weather;
+
+sun;
+// => '☀️'
+
+tornado;
+// => '🌪️'
+
+snowy;
+// => '🌨️'
+```
+
+The following is observed:
+
+- `sun` has extracted from the object `weather` without replacing it as it is present in the object `weather`,
+- `tornado` does not exist in the object `weather`, so the default value was used,
+- `cloud_with_snow` was extracted as the variable `snowy`, without replacing it, as `cloud_with_snow` is present in the object `weather`.
+
+## Destructuring function parameters
+
+The `weather` object has a lot of properties.
+It is possible to directly extract one or multiple properties from this object when it's passed to a function:
+
+```javascript
+function weatherReport({ sun }) {
+ console.log(sun);
+}
+
+weatherReport(sun);
+// => '☀️'
+```
+
+## Destructuring `for of` iteration
+
+When iterating over an `array` (or other iterable), and the items are objects, it is possible to destructure inside the `for (...) of iterable` statement:
+
+```javascript
+const { sun: sunny, cloud: cloudy, cloud_with_rain: rainy } = weather;
+
+const prediction = [
+ {
+ chance: 0.8,
+ weather: sunny,
+ description: 'There is a 80% chance it will remain sunny.',
+ },
+ {
+ chance: 0.15,
+ weather: cloudy,
+ description:
+ 'There is a 15% chance the clouds will keep the sun from poking through.',
+ },
+ {
+ chance: 0.05,
+ weather: rainy,
+ description: 'There is a small chance of rain.',
+ },
+];
+
+for (const { weather: symbol, description } of prediction) {
+ console.log(`${symbol}: ${description}`);
+}
+
+// '☀️: There is a 80% chance it will remain sunny.'
+// '☁️: There is a 15% chance the clouds will keep the sun from poking through.'
+// '🌧️: There is a small chance of rain.'
+```
+
+## Related concepts
+
+[concept:javascript/array-destructuring]()
+[concept:javascript/rest-and-spread]()
+
+[mdn-object-destructuring]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring#object_destructuring
+[mdn-object-literal]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer
diff --git a/concepts/object-destructuring/introduction.md b/concepts/object-destructuring/introduction.md
new file mode 100644
index 0000000000..77f1b8230a
--- /dev/null
+++ b/concepts/object-destructuring/introduction.md
@@ -0,0 +1,31 @@
+# Introduction
+
+JavaScript's object destructuring syntax is a concise way to extract properties from an object and assign them to distinct variables.
+
+In this example, weather symbols are extracted from the object `weather`:
+
+```javascript
+const weather = {
+ sun: '☀️',
+ sun_behind_small_cloud: '🌤️',
+ sun_behind_cloud: '⛅',
+ sun_behind_large_cloud: '🌥️',
+ sun_behind_rain_cloud: '🌦️',
+ cloud: '☁️',
+ cloud_with_rain: '🌧️',
+ cloud_with_snow: '🌨️',
+ cloud_with_lightning: '🌩️',
+ cloud_with_lightning_and_rain: '⛈️',
+};
+
+const { sun, cloud, cloud_with_lightning } = weather;
+
+sun;
+// => '☀️'
+
+cloud;
+// => '☁️'
+
+cloud_with_lightning;
+// => '🌩️'
+```
diff --git a/concepts/object-destructuring/links.json b/concepts/object-destructuring/links.json
new file mode 100644
index 0000000000..b4446d2b1e
--- /dev/null
+++ b/concepts/object-destructuring/links.json
@@ -0,0 +1,10 @@
+[
+ {
+ "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring#object_destructuring",
+ "description": "MDN: Object destructuring"
+ },
+ {
+ "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer",
+ "description": "MDN: Object initializer (and literals)"
+ }
+]
diff --git a/concepts/objects/introduction.md b/concepts/objects/introduction.md
index 824ab75a11..2efc612df4 100644
--- a/concepts/objects/introduction.md
+++ b/concepts/objects/introduction.md
@@ -76,6 +76,7 @@ obj[key];
Using the dot notation as a shorthand has the same restriction as omitting the quotation marks.
It only works if the key follows the identifier naming rules.
+In other words, if your key is not a single word using only letters, you must use square bracket notation.
## Adding or Changing a Value
diff --git a/concepts/promises/about.md b/concepts/promises/about.md
index f62792887e..26f6ac2f7e 100644
--- a/concepts/promises/about.md
+++ b/concepts/promises/about.md
@@ -1,146 +1,273 @@
# About
-The Promise object represents the eventual completion (or failure) of an asynchronous operation and its resulting value. It allows you to associate handlers with an asynchronous action's eventual success value or failure reason.
+The [`Promise`][promise-docs] object represents the eventual completion (or failure) of an asynchronous operation and its resulting value.
-A Promise is in one of these states:
+
+~~~exercism/note
+This is a hard topic for many people, specially if you know programming in a language that is completely _synchronous_.
+If you feel overwhelmed, or you would like to learn more about **concurrency** and **parallelism**, [watch (via go.dev)][talk-blog] or [watch directly via vimeo][talk-video] and [read the slides][talk-slides] of the brilliant talk "Concurrency is not parallelism".
-- **pending**: initial state, neither fulfilled nor rejected.
-- **fulfilled**: meaning that the operation was completed successfully and the result is available.
-- **rejected**: meaning that the operation failed.
+[talk-slides]: https://go.dev/talks/2012/waza.slide#1
+[talk-blog]: https://go.dev/blog/waza-talk
+[talk-video]: https://vimeo.com/49718712
+~~~
-When either of these options happens, the associated handlers queued up by a promise's `then` method is called. If the promise has already been fulfilled or rejected when a corresponding handler is attached, the handler will be called, so there is no race condition between an asynchronous operation completing and its handlers being attached.
+## Lifecycle of a promise
-Example:
+A `Promise` has three states:
+
+1. pending
+2. fulfilled
+3. rejected
+
+When it is created, a promise is pending.
+At some point in the future it may _resolve_ or _reject_.
+Once a promise is resolved or rejected once, it can never be resolved or rejected again, nor can its state change.
+
+In other words:
+
+1. When pending, a promise:
+ - may transition to either the fulfilled or rejected state.
+2. When fulfilled, a promise:
+ - must not transition to any other state.
+ - must have a value, which must not change.
+3. When rejected, a promise:
+ - must not transition to any other state.
+ - must have a reason, which must not change.
+
+## Resolving a promise
+
+A promise may be resolved in various ways:
```javascript
-const myPromise = new Promise(function (resolve, reject) {
- setTimeout(function (resolve, reject) {
- resolve('Jack');
- }, 300);
+// Creates a promise that is immediately resolved
+Promise.resolve(value);
+
+// Creates a promise that is immediately resolved
+new Promise((resolve) => {
+ resolve(value);
});
-myPromise.then(function (e) {
- console.log(e); // expected output 'Jack'
+
+// Chaining a promise leads to a resolved promise
+somePromise.then(() => {
+ // ...
+ return value;
});
```
-## Instance Methods of a Promise
+In the examples above `value` can be _anything_, including an error, `undefined`, `null` or another promise.
+Usually you want to resolve with a value that's not an error.
-### then
+## Rejecting a promise
-> The `.then()` method takes up to two arguments; the first argument is a callback function for the resolved case of the promise, and the second argument is a callback function for the rejected case. Each `.then()` returns a newly generated promise object, which can optionally be used for chaining.[^1]
+A promise may be rejected in various ways:
+
+```javascript
+// Creates a promise that is immediately rejected
+Promise.reject(reason)
+
+// Creates a promise that is immediately rejected
+new Promise((_, reject) {
+ reject(reason)
+})
+
+// Chaining a promise with an error leads to a rejected promise
+somePromise.then(() => {
+ // ...
+ throw reason
+})
+```
+
+In the examples above `reason` can be _anything_, including an error, `undefined` or `null`.
+Usually you want to reject with an error.
+
+## Chaining a promise
+
+A promise may be _continued_ with a future action once it resolves or rejects.
+
+- [`promise.then()`][promise-then] is called once `promise` resolves
+- [`promise.catch()`][promise-catch] is called once `promise` rejects
+- [`promise.finally()`][promise-finally] is called once `promise` either resolves or rejects
+
+### **then**
+
+Every promise is "thenable".
+That means that there is a function `then` available that will be executed once the original promise is resolves.
+Given `promise.then(onResolved)`, the callback `onResolved` receives the value the original promise was resolved with.
+This will always return a _new_ "chained" promise.
+
+Returning a `value` from `then` resolves the "chained" promise.
+Throwing a `reason` in `then` rejects the "chained" promise.
```javascript
const promise1 = new Promise(function (resolve, reject) {
- resolve('Success!');
+ setTimeout(() => {
+ resolve('Success!');
+ }, 1000);
});
-promise1.then(function (value) {
+
+const promise2 = promise1.then(function (value) {
console.log(value);
// expected output: "Success!"
+
+ return true;
});
```
-### catch
+This will log `"Success!"` after approximately 1000 ms.
+The state & value of `promise1` will be `resolved` and `"Success!"`.
+The state & value of `promise2` will be `resolved` and `true`.
-> A `.catch()` is really just a `.then()` without a slot for a callback function for the case when the promise is resolved. It is used to handle rejected promises.[^2]
+There is a second argument available that runs when the original promise rejects.
+Given `promise.then(onResolved, onRejected)`, the callback `onResolved` receives the value the original promise was resolved with, or the callback `onRejected` receives the reason the promise was rejected.
```javascript
-const promise1 = new Promise((resolve, reject) => {
- throw 'An error occured';
-});
-promise1.catch(function (error) {
- console.error(error);
-});
-// expected output: An error occured
-```
+const promise1 = new Promise(function (resolve, reject) {
+ setTimeout(() => {
+ resolve('Success!');
+ }, 1000);
-### finally
+ if (Math.random() < 0.5) {
+ reject('Nope!');
+ }
+});
-> When the promise is settled, i.e either fulfilled or rejected, the specified callback function is executed. This provides a way for code to be run whether the promise was fulfilled successfully or rejected once the Promise has been dealt with.[^3]
+function log(value) {
+ console.log(value);
+ return true;
+}
-```javascript
-function findDataById(id) {
- return new Promise(function (resolve, reject) {
- let sampleData = [1, 2, 3, 4, 5];
- if (sampleData[id]) {
- resolve(sampleData[id]);
- } else {
- reject(new Error('Invalid id'));
- }
- });
+function shout(reason) {
+ console.error(reason.toUpperCase());
+ return false;
}
-findDataById(4)
- .then(function (response) {
- console.log(response);
- })
- .catch(function (err) {
- console.error(err);
- })
- .finally(function () {
- console.log('Promise completed');
- });
+
+const promise2 = promise1.then(log, shout);
```
-## Static Methods of the Promise Class
+- In about 1/2 of the cases, this will log `"Success!"` after approximately 1000 ms.
+ - The state & value of `promise1` will be `resolved` and `"Success!"`.
+ - The state & value of `promise2` will be `resolved` and `true`.
+- In about 1/2 of the cases, this will immediately log `"NOPE!"`.
+ - The state & value of `promise1` will be `rejected` and `Nope!`.
+ - The state & value of `promise2` will be `resolved` and `false`.
+
+It is important to understand that because of the rules of the lifecycle, when it `reject`s, the `resolve` that comes in ~1000ms later is silently ignored, as the internal state cannot change once it has rejected or resolved.
+It is important to understand that returning a value from a promise resolves it, and throwing a value rejects it.
+When `promise1` resolves and there is a chained `onResolved`: `then(onResolved)`, then that follow-up is a new promise that can resolve or reject.
+When `promise1` rejects but there is a chained `onRejected`: `then(, onRejected)`, then that follow-up is a new promise that can resolve or reject.
-Some of the [static methods][promise-static-methods] that are available on `Promise` can be used to resolve and reject promises. Here are a few of them:
+### **catch**
-### Promise.all
+Sometimes you want to capture errors and only continue when the original promise `reject`s.
+Given `promise.catch(onCatch)`, the callback `onCatch` receives the reason the original promise was rejected.
+This will always return a _new_ "chained" promise.
-> The `Promise.all()` method takes an iterable of promises as an input and returns a single Promise that resolves to an array of the results of the input promises. It rejects immediately upon any of the input promises rejecting or non-promises throwing an error and will reject with this first rejection message / error.[^4]
+Returning a `value` from `catch` resolves the "chained" promise.
+Throwing a `reason` in `catch` rejects the "chained" promise.
```javascript
-var p1 = Promise.resolve(10);
-var p2 = 45;
-var p3 = new Promise(function (resolve, reject) {
- setTimeout(function () {
- resolve('Jill');
- }, 300);
-});
-Promise.all([p1, p2, p3]).then(function (values) {
- console.log(values); // => [10, 45, "Jill"]
+const promise1 = new Promise(function (resolve, reject) {
+ setTimeout(() => {
+ resolve('Success!');
+ }, 1000);
+
+ if (Math.random() < 0.5) {
+ reject('Nope!');
+ }
});
-```
-### Promise.reject
+function log(value) {
+ console.log(value);
+ return 'done';
+}
-> The `Promise.reject()` method returns a Promise object that is rejected with a given reason.[^5]
+function recover(reason) {
+ console.error(reason.toUpperCase());
+ return 42;
+}
-```javascript
-Promise.reject(new Error('failed')).then(
- function () {
- // not called
- },
- function (error) {
- console.error(error); // error in the console
- }
-);
+const promise2 = promise1.catch(recover).then(log);
```
-### Promise.resolve
+In about 1/2 of the cases, this will log `"Success!"` after approximately 1000 ms.
+In the other 1/2 of the cases, this will immediately log `42`.
-> The `Promise.resolve()` method returns a Promise object that is resolved with a given value. If the value is a promise, that promise is returned; if the value is a thenable (i.e. has a "then" method), the returned promise will "follow" that thenable, adopting its eventual state; otherwise the returned promise will be fulfilled with the value.[^6]
+- If `promise1` resolves, `catch` is skipped and it reaches `then`, and logs the value.
+ - The state & value of `promise1` will be `resolved` and `"Success!"`.
+ - The state & value of `promise2` will be `resolved` and `"done"`;
+- If `promise1` rejects, `catch` is executed, which _returns a value_, and thus the chain is now `resolved`, and it reaches `then`, and logs the value.
+ - The state & value of `promise1` will be `rejected` and `"Nope!"`.
+ - The state & value of `promise2` will be `resolved` and `"done"`;
+
+### **finally**
+
+Sometimes you want to execute code after a promise settles, regardless if the promise resolves or rejects.
+Given `promise.finally(onSettled)`, the callback `onSettled` receives nothing.
+This will always return a _new_ "chained" promise.
+
+Returning a `value` from `finally` copies the status & value from the original promise, ignoring the `value`.
+Throwing a `reason` in `finally` rejects the "chained" promise, overwriting any status & value or reason from the original promise.
+
+## Example
+
+Various of the methods together:
```javascript
-Promise.resolve('resolved!').then(
- function (value) {
- console.log(value); // "resolved!"
- },
- function (value) {
- // not called
+const myPromise = new Promise(function (resolve, reject) {
+ const sampleData = [2, 4, 6, 8];
+ const randomNumber = Math.round(Math.random() * 5);
+
+ if (sampleData[randomNumber]) {
+ resolve(sampleData[randomNumber]);
+ } else {
+ reject('Sampling did not result in a sample');
}
-);
+});
+
+const finalPromise = myPromise
+ .then(function (sampled) {
+ // If the random number was 0, 1, 2, or 3, this will be
+ // reached and the number 2, 4, 6, or 8 will be logged.
+ console.log(`Sampled data: ${sampled}`);
+ return 'yay';
+ })
+ .catch(function (reason) {
+ // If the random number was 4 or 5, this will be reached and
+ // reason will be "An error occurred". The entire chain will
+ // then reject with an Error with the reason as message.
+ throw new Error(reason);
+ })
+ .finally(function () {
+ // This will always log after either the sampled data is
+ // logged or the error is raised.
+ console.log('Promise completed');
+ });
```
----
+- In the cases `randomNumber` is `0-3`:
+ - `myPromise` will be resolved with the value `2, 4, 6, or 8`
+ - `finalPromise` will be resolved with the value `'yay'`
+ - There will be two logs:
+ - `Sampled data: ...`
+ - `Promise completed`
+- In the cases `randomNumber` is `4-5`:
+ - `myPromise` will be rejected with the reason `'Sampling did not result in a sample'`
+ - `finalPromise` will be rejected with the reason `Error('Sampling did not result in a sample')`
+ - There will be one log:
+ - `Promise completed`
+ - _in some environments_ this will yield an `"uncaught rejected promise: Error('Sampling did not result in a sample')"` log
+
+As shown above, `reject` works with a string, and a promise can also reject with an `Error`.
+
+
+~~~exercism/note
+If chaining promises or general usage is unclear, the [tutorial on MDN][mdn-promises] is a good resource to consume.
-[^4]: `all`, MDN.
-[^5]: `reject`, MDN.
-[^6]: `resolve`, MDN.
-[^1]: `then`, MDN.
-[^2]: `catch`, MDN.
-[^3]: `finally`, MDN.
+[mdn-promises]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises
+~~~
+[promise-docs]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
[promise-catch]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch
[promise-then]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then
[promise-finally]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally
-[promise-static-methods]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#static_methods
diff --git a/concepts/promises/introduction.md b/concepts/promises/introduction.md
index ad635f6b47..3b1440a5d8 100644
--- a/concepts/promises/introduction.md
+++ b/concepts/promises/introduction.md
@@ -1,104 +1,50 @@
# Introduction
-The [`Promise`][promise-docs] object represents the eventual completion (or failure) of an
-asynchronous operation, and its resulting value.
+The [`Promise`][promise-docs] object represents the eventual completion (or failure) of an asynchronous operation and its resulting value.
-The methods [`promise.then()`][promise-then], [`promise.catch()`][promise-catch], and [`promise.finally()`][promise-finally] are used to associate further action with a promise that becomes settled.
+
+~~~exercism/note
+This is a hard topic for many people, especially if you know programming in a language that is completely _synchronous_.
+If you feel overwhelmed, or you would like to learn more about **concurrency** and **parallelism**, [watch (via go.dev)][talk-blog] or [watch directly via vimeo][talk-video] and [read the slides][talk-slides] of the brilliant talk "Concurrency is not parallelism".
-For example:
+[talk-slides]: https://go.dev/talks/2012/waza.slide#1
+[talk-blog]: https://go.dev/blog/waza-talk
+[talk-video]: https://vimeo.com/49718712
+~~~
-```javascript
-const myPromise = new Promise(function (resolve, reject) {
- let sampleData = [2, 4, 6, 8];
- let randomNumber = Math.floor(Math.random() * (sampleData.length + 1));
- if (sampleData[randomNumber]) {
- resolve(sampleData[randomNumber]);
- } else {
- reject('An error occured!');
- }
-});
+## Lifecycle of a promise
-myPromise
- .then(function (e) {
- console.log(e);
- })
- .catch(function (error) {
- throw new Error(error);
- })
- .finally(function () {
- console.log('Promise completed');
- });
-```
+A `Promise` has three states:
-## Methods
+1. pending
+2. fulfilled
+3. rejected
-These methods are available on `Promise.prototype`
+When it is created, a promise is pending.
+At some point in the future it may _resolve_ or _reject_.
+Once a promise is resolved or rejected once, it can never be resolved or rejected again, nor can its state change.
-**then**
+In other words:
-> The `.then()` method takes up to two arguments; the first argument is a callback function for the resolved case of the promise, and the second argument is a callback function for the rejected case. Each `.then()` returns a newly generated promise object, which can optionally be used for chaining.[^1]
+1. When pending, a promise:
+ - may transition to either the fulfilled or rejected state.
+2. When fulfilled, a promise:
+ - must not transition to any other state.
+ - must have a value, which must not change.
+3. When rejected, a promise:
+ - must not transition to any other state.
+ - must have a reason, which must not change.
-```javascript
-const promise1 = new Promise(function (resolve, reject) {
- resolve('Success!');
-});
+## Chaining promises
-promise1.then(function (value) {
- console.log(value);
- // expected output: "Success!"
-});
-```
+In JavaScript, there are various methods to chain promises.
+Calling a chaining method on a promise returns another promise.
-**catch**
+
+~~~exercism/note
+The [tutorial on MDN][mdn-promises] is a good resource to consume if you want to learn more about promises before completing the concept exercise.
-> A `.catch()` is really just a `.then()` without a slot for a callback function for the case when the promise is resolved. It is used to handle rejected promises.[^2]
-
-```javascript
-const promise1 = new Promise((resolve, reject) => {
- throw 'An error occured';
-});
-
-promise1.catch(function (error) {
- console.error(error);
-});
-// expected output: An error occured
-```
-
-**finally**
-
-> When the promise is settled, i.e either fulfilled or rejected, the specified callback function is executed. This provides a way for code to be run whether the promise was fulfilled successfully or rejected once the Promise has been dealt with.[^3]
-
-```javascript
-function findDataById(id) {
- return new Promise(function (resolve, reject) {
- let sampleData = [1, 2, 3, 4, 5];
- if (sampleData[id]) {
- resolve(sampleData[id]);
- } else {
- reject(new Error('Invalid id'));
- }
- });
-}
-
-findDataById(4)
- .then(function (response) {
- console.log(response);
- })
- .catch(function (err) {
- console.error(err);
- })
- .finally(function () {
- console.log('Promise completed');
- });
-```
-
----
-
-[^1]: `then`, MDN.
-[^2]: `catch`, MDN.
-[^3]: `finally`, MDN.
+[mdn-promises]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises
+~~~
[promise-docs]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
-[promise-catch]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch
-[promise-then]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then
-[promise-finally]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally
diff --git a/concepts/randomness/.meta/config.json b/concepts/randomness/.meta/config.json
new file mode 100644
index 0000000000..1b2ce91b6f
--- /dev/null
+++ b/concepts/randomness/.meta/config.json
@@ -0,0 +1,5 @@
+{
+ "blurb": "Random number generation using Math.random()",
+ "authors": ["SneakyMallard"],
+ "contributors": ["SleeplessByte"]
+}
diff --git a/concepts/randomness/about.md b/concepts/randomness/about.md
new file mode 100644
index 0000000000..7898ab4f1e
--- /dev/null
+++ b/concepts/randomness/about.md
@@ -0,0 +1,61 @@
+# Introduction
+
+Many programs need (pseudo-)random values to simulate real-world events.
+
+Common, familiar examples include:
+
+- A coin toss: a random value from ('H', 'T').
+- The roll of a die: a random integer from 1 to 6.
+- Shuffling a deck of cards: a random ordering of a card list.
+- The creation of trees and bushes in a 3-D graphics simulation.
+
+Generating truly random values with a computer is a [surprisingly difficult technical challenge][why-randomness-is-hard], which is why there are also "pseudorandom" generators.
+
+
+~~~exercism/advanced
+[The language specification][spec] for JavaScript doesn't force the implementation for random number generation.
+All major browsers and JavaScript runtimes implement a PRNG (pseudo-random number generator).
+Because the numbers are not cryptographically secure, they should never be used for anything that requires true or at least cryptographically secure random numbers, such as certificate or password generation or operations.
+
+There is a standard called [Web Cryptography][rfc] which standardizes an interface for doing cryptography in JavaScript.
+It is implemented [by Browsers][crypto-web] as well as runtimes such as [Node.JS][crypto-node] and [Deno][crypto-deno].
+
+This concept is not about Web Crypto and will restrict itself to pseudo-random number generation.
+
+[rfc]: https://www.w3.org/TR/webcrypto-2/
+[spec]: https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-math.random
+[crypto-web]: https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues
+[crypto-node]: https://nodejs.org/api/webcrypto.html#cryptogetrandomvaluestypedarray
+[crypto-deno]: https://docs.deno.com/api/web/~/Crypto
+~~~
+
+## Generating random numbers
+
+In Javascript, you can generate pseudorandom numbers using the [`Math.random()`][Math.random] function.
+It will return a pseudorandom floating-point number between 0 (inclusive), and 1 (exclusive).
+
+To get a random number between _min_ (inclusive) and _max_ (exclusive) you can use a function something like this:
+
+```javascript
+function getRandomInRange(min, max) {
+ return min + Math.random() * (max - min);
+}
+getRandomInRange(4, 10);
+// => 5.72
+```
+
+
+~~~exercism/advanced
+Most simple techniques of returning a range of numbers based on the randomly generated number [will introduce bias][bias].
+That means that some numbers will be more likely to be rolled than others.
+Using the multiplication technique spreads out the bias over the entire range, so it will be less obvious and in most cases not a big issue, but you should be aware of this.
+
+[bias]: https://adammil.net/blog/v134_Efficiently_generating_random_numbers_without_bias.html
+~~~
+
+## Generating random integers
+
+To generate a random integer, you can use `Math.floor()` or `Math.ceil()` to turn a randomly generated number into an integer.
+
+[why-randomness-is-hard]: https://www.malwarebytes.com/blog/news/2013/09/in-computers-are-random-numbers-really-random
+[Math.random]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random
diff --git a/concepts/randomness/introduction.md b/concepts/randomness/introduction.md
new file mode 100644
index 0000000000..4cd34460f5
--- /dev/null
+++ b/concepts/randomness/introduction.md
@@ -0,0 +1,40 @@
+# Introduction
+
+Many programs need (pseudo-)random values to simulate real-world events.
+
+Common, familiar examples include:
+
+- A coin toss: a random value from ('H', 'T').
+- The roll of a die: a random integer from 1 to 6.
+- Shuffling a deck of cards: a random ordering of a card list.
+- The creation of trees and bushes in a 3-D graphics simulation.
+
+Generating truly random values with a computer is a [surprisingly difficult technical challenge][why-randomness-is-hard], which is why there are also "pseudorandom" generators.
+
+
+~~~exercism/caution
+The `Math.random()` function should NOT be used for security and cryptographic applications!
+Finish the learning exercise(s) about this concept to learn more
+~~~
+
+## Generating random numbers
+
+In Javascript, you can generate pseudorandom numbers using the [`Math.random()`][Math.random] function.
+It will return a pseudorandom floating-point number between 0 (inclusive), and 1 (exclusive).
+
+To get a random number between _min_ (inclusive) and _max_ (exclusive) you can use a function something like this:
+
+```javascript
+function getRandomInRange(min, max) {
+ return min + Math.random() * (max - min);
+}
+getRandomInRange(4, 10);
+// => 5.72
+```
+
+## Generating random integers
+
+To generate a random integer, you can use `Math.floor()` or `Math.ceil()` to turn a randomly generated number into an integer.
+
+[why-randomness-is-hard]: https://www.malwarebytes.com/blog/news/2013/09/in-computers-are-random-numbers-really-random
+[Math.random]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random
diff --git a/concepts/randomness/links.json b/concepts/randomness/links.json
new file mode 100644
index 0000000000..5d949ca1a5
--- /dev/null
+++ b/concepts/randomness/links.json
@@ -0,0 +1,6 @@
+[
+ {
+ "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random",
+ "description": "MDN: The Math.random() function"
+ }
+]
diff --git a/concepts/recursion/.meta/config.json b/concepts/recursion/.meta/config.json
index 0095ad370a..b2b734c22a 100644
--- a/concepts/recursion/.meta/config.json
+++ b/concepts/recursion/.meta/config.json
@@ -1,5 +1,5 @@
{
"blurb": "Recursive functions are functions that call themselves.",
- "authors": ["SleeplessByte"],
+ "authors": ["SleeplessByte", "zynthatrix"],
"contributors": []
}
diff --git a/concepts/recursion/about.md b/concepts/recursion/about.md
index 4b51d644bd..051bd2d3cb 100644
--- a/concepts/recursion/about.md
+++ b/concepts/recursion/about.md
@@ -1 +1,93 @@
-# About
+# Understanding Recursion in JavaScript
+
+Recursion is a powerful concept in programming that involves a function calling itself.
+It can be a bit tricky to grasp at first, but once you understand the fundamentals, it becomes a valuable tool in solving complex problems.
+We'll explore recursion in JavaScript with easy-to-understand examples.
+
+## What is Recursion?
+
+Recursion occurs when a function calls itself, either directly or indirectly.
+It's similar to a loop, but it may involve breaking a problem down into smaller, more manageable sub-problems.
+
+### Example 1: Countdown
+
+Let's start with a simple example: a countdown function.
+
+```javascript
+function countdown(num) {
+ // Base case
+ if (num <= 0) {
+ console.log('Blastoff!');
+ return;
+ }
+
+ // Recursive case
+ console.log(num);
+ countdown(num - 1);
+}
+
+// Call the function
+countdown(5);
+```
+
+In this example:
+
+- **Base case**: When `num` becomes less than or equal to 0, the function prints "Blastoff!" and stops calling itself.
+- **Recursive case**: The function prints the current `num` and calls itself with `num - 1`.
+
+### Example 2: Factorial
+
+Now, let's look at a classic example of recursion: calculating the factorial of a number.
+
+```javascript
+function factorial(n) {
+ // Base case
+ if (n === 0 || n === 1) {
+ return 1;
+ }
+
+ // Recursive case
+ return n * factorial(n - 1);
+}
+
+// Test the function
+console.log(factorial(5)); // Output: 120
+```
+
+In this example:
+
+- **Base case**: When `n` is 0 or 1, the function returns 1.
+- **Recursive case**: The function multiplies `n` by the factorial of `n - 1`.
+
+## Key Concepts
+
+### Base Case
+
+Every recursive function should have at least one base case, a condition where the function stops calling itself.
+Without a base case, the recursion would continue indefinitely, leading to a stack overflow.
+
+### Recursive Case
+
+The recursive case defines how the function calls itself with a smaller or simpler version of the problem.
+
+## Pros and Cons of Recursion
+
+**Pros:**
+
+- Elegant solution for certain problems.
+- Mimics the mathematical induction concept.
+
+**Cons:**
+
+- Can be less efficient than iterative solutions.
+- May lead to stack overflow for deep recursion.
+
+## Conclusion
+
+Recursion is a valuable technique that simplifies complex problems by breaking them into smaller, more manageable sub-problems.
+Understanding base cases and recursive cases is crucial for implementing effective recursive solutions in JavaScript.
+
+**Learn More:**
+
+- [MDN: Recursion in JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions#recursion)
+- [Eloquent JavaScript: Chapter 3 - Functions](https://eloquentjavascript.net/03_functions.html)
diff --git a/concepts/recursion/introduction.md b/concepts/recursion/introduction.md
index 83d31dab92..74c281ea7f 100644
--- a/concepts/recursion/introduction.md
+++ b/concepts/recursion/introduction.md
@@ -1,3 +1,93 @@
-# Introduction
+# Understanding Recursion in JavaScript
-TODO: add introduction for recursion concept
+Recursion is a powerful concept in programming that involves a function calling itself.
+It can be a bit tricky to grasp at first, but once you understand the fundamentals, it becomes a valuable tool in solving complex problems.
+We'll explore recursion in JavaScript with easy-to-understand examples.
+
+## What is Recursion?
+
+Recursion occurs when a function calls itself, either directly or indirectly.
+It's similar to a loop, but it involves breaking a problem down into smaller, more manageable sub-problems.
+
+### Example 1: Countdown
+
+Let's start with a simple example: a countdown function.
+
+```javascript
+function countdown(num) {
+ // Base case
+ if (num <= 0) {
+ console.log('Blastoff!');
+ return;
+ }
+
+ // Recursive case
+ console.log(num);
+ countdown(num - 1);
+}
+
+// Call the function
+countdown(5);
+```
+
+In this example:
+
+- **Base case**: When `num` becomes less than or equal to 0, the function prints "Blastoff!" and stops calling itself.
+- **Recursive case**: The function prints the current `num` and calls itself with `num - 1`.
+
+### Example 2: Factorial
+
+Now, let's look at a classic example of recursion: calculating the factorial of a number.
+
+```javascript
+function factorial(n) {
+ // Base case
+ if (n === 0 || n === 1) {
+ return 1;
+ }
+
+ // Recursive case
+ return n * factorial(n - 1);
+}
+
+// Test the function
+console.log(factorial(5)); // Output: 120
+```
+
+In this example:
+
+- **Base case**: When `n` is 0 or 1, the function returns 1.
+- **Recursive case**: The function multiplies `n` by the factorial of `n - 1`.
+
+## Key Concepts
+
+### Base Case
+
+Every recursive function should have at least one base case, a condition where the function stops calling itself.
+Without a base case, the recursion would continue indefinitely, leading to a stack overflow.
+
+### Recursive Case
+
+The recursive case defines how the function calls itself with a smaller or simpler version of the problem.
+
+## Pros and Cons of Recursion
+
+**Pros:**
+
+- Elegant solution for certain problems.
+- Mimics the mathematical induction concept.
+
+**Cons:**
+
+- Can be less efficient than iterative solutions.
+- May lead to stack overflow for deep recursion.
+
+## Conclusion
+
+Recursion is a valuable technique that simplifies complex problems by breaking them into smaller, more manageable sub-problems.
+Understanding base cases and recursive cases is crucial for implementing effective recursive solutions in JavaScript.
+
+**Learn More:**
+
+- [MDN: Recursion in JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions#recursion)
+- [Eloquent JavaScript: Chapter 3 - Functions](https://eloquentjavascript.net/03_functions.html)
diff --git a/concepts/regular-expressions/.meta/config.json b/concepts/regular-expressions/.meta/config.json
new file mode 100644
index 0000000000..43444d6a9a
--- /dev/null
+++ b/concepts/regular-expressions/.meta/config.json
@@ -0,0 +1,5 @@
+{
+ "blurb": "JavaScript supports regular expressions as a powerful way of manipulating and searching through data.",
+ "authors": ["julendiaz"],
+ "contributors": ["junedev"]
+}
diff --git a/concepts/regular-expressions/about.md b/concepts/regular-expressions/about.md
new file mode 100644
index 0000000000..3bf86f7aa1
--- /dev/null
+++ b/concepts/regular-expressions/about.md
@@ -0,0 +1,189 @@
+# About
+
+## Regular Expressions in JavaScript
+
+A [Regular Expression][mdn-regular-expressions] (or Regex) is a sequence of characters that we can use to target and manipulate certain elements in strings. Hence, we can:
+
+- **Search** for a text in a string
+- **Replace** substrings in a string
+- **Extract** information from a string
+
+This concept is focused on using regular expressions in JavaScript. If you want to learn about how to write regular expressions, [Eloquent JavaScript][eloquent-javascript] might be a good resource.
+
+> 💡 JavaScript's regex flavor is part of the ECMA-262 standard for the language. This means that you don't have to worry about browser-specific compatibility.
+
+## How to create Regular Expressions
+
+In JavaScript, regular expressions are mostly written in the format `/pattern/modifiers` where 'pattern' is the main regex, and 'modifiers' are a series of characters which we can add to indicate special options (like superpowers).
+
+Nevertheless, we have two ways of creating a regular expression:
+
+1. **Regular Expression Literal**:
+
+```javascript
+const regex = /[a-z]/;
+```
+
+2. Constructor **RegExp**:
+
+```javascript
+const regex = new RegExp('[a-z]');
+```
+
+In both cases, JavaScript is creating an object out of the regex. It is recommended to use immutable patterns with the literal as default.
+
+The RegExp constructor can be used for cases where the regex will change or we don't know it yet (like an input).
+
+## 🏴☠️ Flags
+
+Regular expressions have optional superpowers called [`flags`][mdn-flags] that allow for additional features. These flags can be used separately or together in any order, and are included as part of the regular expression.
+
+Some of the widely used are:
+
+- `/g` - Global Search
+- `/i` - Case Insensitive
+- `/m` - Multiline Search
+
+Here is a simple example:
+
+```javascript
+const re = /home/gi;
+const str = 'Home, sweet home.';
+
+str.match(re);
+
+// => ["Home", "home"]
+```
+
+The `g` character allows us to parse all possible matches within a string. Without this feature, JavaScript would have extracted only the first `Home` match.
+
+The Case Insensitive flag `/i` enables us to be flexible when it comes to finding a pattern. This way it doesn't matter if what we are looking for is in `UPPERCASE` or `lowercase`.
+
+When using the `RegExp` constructor, the syntax of adding flags is different. In this case, we just need to pass the flags as a second argument.
+
+## Commonly used functions
+
+When regular expressions are combined with current build-in functions in JavaScript, we have some powerful ways of manipulating and getting data from strings.
+
+The `test()` method is a great way of searching and knowing if a target value exists within a given string. In this way, it returns a boolean value, `true` or `false`.
+
+The `match()` method extracts the regular expression match from a given string. Returning an array with information and matches.
+
+_When to use one or the other?_ Use the `test()` when you want a fast way of checking a value within a string, use `match()` if you need to use that value or want more information about the match.
+
+### Test
+
+The [test()][mdn-regex-test] method executes a search for a match between a regular expression and a specified string. Returns true or false.
+
+```javascript
+const str = 'It is difficult to test if you have a virus';
+const result = /virus$/.test(str);
+
+console.log(result);
+
+// => true
+```
+
+### Match
+
+Okey but, what if we want to get the actual values of the regular expression search?
+
+Instead of returning just a boolean, with the [match()][mdn-regex-match] method, we get a useful array whose contents depend on the presence or absence of the found matches.
+
+In this way, we are able both to **search** and to **extract** information from any string. For example:
+
+```javascript
+const funnyQuote =
+ 'If you see someone crying, ask if it is because of their haircut.';
+const regex1 = /someone/;
+const regex2 = /happy/;
+
+funnyQuote.match(regex1);
+// => ["someone", index: 3, input: "If you see someone crying, ask if it is because of their haircut.", groups: undefined]
+
+funnyQuote.match(regex2);
+// => null
+```
+
+When the Global Search flag `/g` is present, instead of getting the only match alongside useful information such as the index or input, the method returns all of the occurrences present in the array:
+
+```javascript
+const funnyQuote =
+ 'If you see someone crying, ask if it is because of their haircut.';
+
+const regex3 = /if/gi;
+
+funnyQuote.match(regex3);
+// => ["If", "if"];
+```
+
+### Replace
+
+The [replace()][mdn-regex-replace] method in JavaScript allows us to search for a value within a given string, and replace it with a new value.
+
+```javascript
+string.replace(searchValue, newValue);
+```
+
+The pattern for searching the substitution can be a single string, or a regular expression.
+
+```javascript
+let string = 'I like dogs!';
+let result = string.replace('dogs', 'cats');
+
+let string = 'I would love to travel to Japan';
+let result = string.replace(/Japan/g, 'Hawaii');
+```
+
+Moreover, we can apply a function on the replacement position in order to make additional changes to each value.
+
+```javascript
+let text = 'Say hello to the chatbot.';
+let result = text.replace(/chatbot|hello/gi, function (word) {
+ return word.toUpperCase();
+});
+// => "Say HELLO to the CHATBOT"
+```
+
+> 💡 In the end, the combination of regular expressions and the `replace()` method is a more dynamic way of replacing elements. Using a single string may be limited.
+
+### Split
+
+The [split()][mdn-regex-split] method in JavaScript represents a different way of using and manipulating strings with regular expressions.
+
+In this way, we will be using regex in order to divide a given string by recognizing a pattern, e.g. `str.split(/[,.\s]/)`. This pattern will be used as the `separator`.
+
+```javascript
+const str = 'hello,user.how are.you';
+
+const result = str.split(/[,.\s]/);
+
+console.log(result);
+// => ['hello', 'user', 'how', 'are', 'you']
+```
+
+## Performance
+
+Regarding performance, both of them create a RegExp object. The main difference is how often the regex is compiled:
+
+- With `Regular Expression Literal`: one time during initial code parsing and compiling
+- With `RegExp()` syntax: Every time new Object gets created.
+
+Using literal syntax may be a better option not only because of performance, but also for simplicity and readability. For more details see this [Stackoverflow discussion][stackoverflow-discussion].
+
+1. It is shorter and doesn’t force you to think in terms of class-like constructors.
+2. When using the `RegExp()` constructor, you also need to escape quotes and double-escape backslashes. It makes regular expressions that are hard to read and understand by their nature even more harder.
+
+[using-regular-expressions-in-javascript]: https://www.regular-expressions.info/javascript.html
+[mdn-regex-cheatsheet]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Cheatsheet
+[mdn-regular-expressions]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
+[mdn-common-functions]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#using_regular_expressions_in_javascript
+[mdn-flags]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#advanced_searching_with_flags
+[mdn-regex-test]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test
+[mdn-regex-match]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match
+[mdn-regex-replace]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace
+[mdn-regex-split]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split
+[demystifying-regular-expressions-with-javascript]: https://livecodestream.dev/post/demystifying-regular-expressions-with-javascript/
+[how-to-use-the-js-replace-method-on-a-string]: https://www.freecodecamp.org/news/javascript-regex-match-example-how-to-use-the-js-replace-method-on-a-string/
+[eloquent-javascript]: https://eloquentjavascript.net/09_regexp.html
+[stackoverflow-discussion]: https://stackoverflow.com/a/32523333
diff --git a/concepts/regular-expressions/introduction.md b/concepts/regular-expressions/introduction.md
new file mode 100644
index 0000000000..f724a70b1a
--- /dev/null
+++ b/concepts/regular-expressions/introduction.md
@@ -0,0 +1,172 @@
+# Introduction
+
+## Regular Expressions in JavaScript
+
+A [Regular Expression][mdn-regular-expressions] (or Regex) is a sequence of characters that we can use to target and manipulate certain elements in strings. Hence, we can:
+
+- **Search** for a text in a string
+- **Replace** substrings in a string
+- **Extract** information from a string
+
+> 💡 JavaScript's regex flavor is part of the ECMA-262 standard for the language. This means that you don't have to worry about browser-specific compatibility.
+
+## How to create Regular Expressions
+
+In JavaScript, a regular expressions is mostly written in the format `/pattern/modifiers`.
+
+We have two ways of creating a regular expression:
+
+1. **Regular Expression Literal**:
+
+```javascript
+const regex = /[a-z]/;
+```
+
+2. Constructor **RegExp**:
+
+```javascript
+const regex = new RegExp('[a-z]');
+```
+
+In both cases, JavaScript is creating an object out of the regex. It is recommended to use immutable patterns with the literal as default.
+
+The RegExp constructor can be used for cases where the regex will change or we don't know it yet (like an input).
+
+## 🏴☠️ Flags
+
+Regular expressions have optional superpowers called [`flags`][mdn-flags] that allow for additional features.
+
+Some of the widely used are:
+
+- `/g` - Global Search
+- `/i` - Case Insensitive
+- `/m` - Multiline Search
+
+Here is a simple example:
+
+```javascript
+const re = /home/gi;
+const str = 'Home, sweet home.';
+const myArray = str.match(re);
+console.log(myArray);
+
+// => ["Home", "home"]
+```
+
+The `g` character allows us to parse all possible matches within a string. Without this feature, JavaScript would have extracted only the first `Home` match.
+
+The Case Insensitive flag `/i` enables us to be flexible when it comes to finding a pattern. This way it doesn't matter if what we are looking for is in `UPPERCASE` or `lowercase`.
+
+When using the `RegExp` constructor, the syntax of adding flags is different.
+
+```javascript
+let regex = /[a-z]/gi; // literal notation
+let regex = new RegExp('[a-z]', 'gi'); // constructor with string pattern as first argument
+let regex = new RegExp(/[a-z]/, 'gi'); // constructor with regular expression literal as first argument (Starting with ECMAScript 6)
+```
+
+## Most common Functions
+
+When regular expressions are combined with current build-in functions in JavaScript, we have some powerful ways of manipulating and getting data from strings.
+
+These are some of the most common functions used alongside regex.
+
+### Test
+
+The [test()][mdn-regex-test]] method executes a search for a match between a regular expression and a specified string. Returns true or false.
+
+```javascript
+const str = 'It is difficult to test if you have a virus';
+const result = /virus$/.test(str);
+
+console.log(result);
+
+// => true
+```
+
+### Match
+
+With the [match()][mdn-regex-match] method, we get a useful array whose contents depend on the presence or absence of the found matches.
+
+In this way, we are able both to **search** and to **extract** information from any string. For example:
+
+```javascript
+const funnyQuote =
+ 'If you see someone crying, ask if it is because of their haircut.';
+const regex1 = /someone/;
+const regex2 = /happy/;
+
+funnyQuote.match(regex1);
+// => ["someone", index: 3, input: "If you see someone crying, ask if it is because of their haircut.", groups: undefined]
+
+funnyQuote.match(regex2);
+// => null
+```
+
+When the Global Search flag `/g` is present, instead of getting the only match alongside useful information such as the index or input, the method returns all of the occurances displayed in the array:
+
+```javascript
+const funnyQuote =
+ 'If you see someone crying, ask if it is because of their haircut.';
+
+const regex3 = /if/gi;
+
+funnyQuote.match(regex3);
+// => ["If", "if"];
+```
+
+### Replace
+
+The [replace()][mdn-regex-replace] method in JavaScript allows us to search for a value within a given string, and replacing it with a desired new value.
+
+```javascript
+string.replace(searchValue, newValue);
+```
+
+The pattern for searching the substitution can be a single string, or a regular expression.
+
+```javascript
+let string = 'I like dogs!';
+let result = string.replace('dogs', 'cats');
+
+let string = 'I would love to travel to Japan';
+let result = string.replace(/Japan/g, 'Hawaii');
+```
+
+Moreover, we can apply a function on the replacement position in order to make additional changes to each value.
+
+```javascript
+let text = 'Say hello to the chatbot.';
+let result = text.replace(/chatbot|hello/gi, function (word) {
+ return word.toUpperCase();
+});
+
+// => "Say HELLO to the CHATBOT"
+```
+
+### Split
+
+The [split()][mdn-regex-split] method in JavaScript represents a different way of using and manipulating strings with regular expressions.
+
+In this way, we will be using regex in order to divide a given string by recognizing a pattern, e.g. `str.split(/[,.\s]/)`. This pattern will be used as the `separator`.
+
+```javascript
+const str = 'hello,user.how are.you';
+
+const result = str.split(/[,.\s]/);
+
+console.log(result);
+// => ['hello', 'user', 'how', 'are', 'you']
+```
+
+[using-regular-expressions-in-javascript]: https://www.regular-expressions.info/javascript.html
+[mdn-regex-cheatsheet]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Cheatsheet
+[mdn-regular-expressions]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
+[mdn-common-functions]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#using_regular_expressions_in_javascript
+[mdn-flags]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#advanced_searching_with_flags
+[mdn-regex-test]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test
+[mdn-regex-match]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match
+[mdn-regex-replace]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace
+[mdn-regex-split]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split
+[demystifying-regular-expressions-with-javascript]: https://livecodestream.dev/post/demystifying-regular-expressions-with-javascript/
+[how-to-use-the-js-replace-method-on-a-string]: https://www.freecodecamp.org/news/javascript-regex-match-example-how-to-use-the-js-replace-method-on-a-string/
diff --git a/concepts/regular-expressions/links.json b/concepts/regular-expressions/links.json
new file mode 100644
index 0000000000..adf9c1f6bd
--- /dev/null
+++ b/concepts/regular-expressions/links.json
@@ -0,0 +1,22 @@
+[
+ {
+ "url": "https://www.regular-expressions.info/javascript.html",
+ "description": "Using regular expressions in Javascript"
+ },
+ {
+ "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Cheatsheet",
+ "description": "MDN: Regex Cheatsheet"
+ },
+ {
+ "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions",
+ "description": "MDN: Regular Expressions"
+ },
+ {
+ "url": "https://livecodestream.dev/post/demystifying-regular-expressions-with-javascript/",
+ "description": "Demystifying regular expressions with JavaScript"
+ },
+ {
+ "url": "https://www.freecodecamp.org/news/javascript-regex-match-example-how-to-use-the-js-replace-method-on-a-string/",
+ "description": "How to use the Replace Method"
+ }
+]
diff --git a/concepts/template-strings/introduction.md b/concepts/template-strings/introduction.md
index 7e54dc9cf9..6af7f9cbc7 100644
--- a/concepts/template-strings/introduction.md
+++ b/concepts/template-strings/introduction.md
@@ -1,6 +1,6 @@
# Introduction
-In JavaScript, _template strings_ allows for embedding expressions in strings, also referred to as string interpolation.
+In JavaScript, _template strings_ allow for embedding expressions in strings, also referred to as string interpolation.
This functionality extends the functionality of the built-in [`String`][string-reference] global object.
You can create template strings in JavaScript by wrapping text in backticks.
@@ -32,6 +32,21 @@ When you are needing to have strings formatted on multiple lines:
`This is an example of using template
strings to accomplish multiple
lines`;
+
+/* => This is an example of using template
+ strings to accomplish multiple
+ lines
+*/
+```
+
+If you want to represent a newline inside a regular string instead of using a template string (ie. not using backticks), you can use the newline escape sequence `\n`:
+
+```javascript
+'This is an example of using the newline escape sequence!\nWithout backticks';
+
+/* => This is an example of using the newline escape sequence!
+ Without backticks
+*/
```
With the available substitution capabilities, you can also introduce logic into the process to determine what the output string should be.
diff --git a/concepts/type-checking/.meta/config.json b/concepts/type-checking/.meta/config.json
new file mode 100644
index 0000000000..72261751ff
--- /dev/null
+++ b/concepts/type-checking/.meta/config.json
@@ -0,0 +1,8 @@
+{
+ "blurb": "Learn how to check the type of a value or object in JavaScript",
+ "authors": [
+ "quintuple-mallard",
+ "SleeplessByte"
+ ],
+ "contributors": []
+}
diff --git a/concepts/type-checking/about.md b/concepts/type-checking/about.md
new file mode 100644
index 0000000000..0afed179a3
--- /dev/null
+++ b/concepts/type-checking/about.md
@@ -0,0 +1,164 @@
+# About
+
+Knowning what the type of a piece of data is, is often very important for code to run smoothly and without errors.
+
+Javascript has several ways to check the type of a value or object.
+
+```exercism/note
+Javascript's type checking mechanisms can be somewhat unreliable.
+
+For better type safety and stronger types, you should probably use TypeScript, a language that builds on JavaScript, but with the type syntax of a static-typed language.
+```
+
+## The `typeof` operator
+
+The `typeof` operator returns the type of its operand.
+The output is a string matching the name of one of the [primitive data types][primitives], except for `"null"`.
+It can also be `"function"` or `"object"`.
+
+```javascript
+typeof undefined;
+// => "undefined"
+
+typeof true;
+// => "boolean"
+
+typeof 42;
+// => "number"
+
+typeof 'Hello, World!';
+// => "string"
+
+typeof function () {
+ return 'Hello, World';
+};
+// => "function"
+
+typeof [1, 2, 3, 4];
+// => "object"
+
+typeof { city: 'Stockholm', country: 'Sweden' };
+// => "object"
+```
+
+For [historical reasons][`typeof null` is `"object"`].
+
+## The `instanceof` operator
+
+For checking the type of an object, you can use the `instanceof` operator.
+It evaluates into a `boolean` depending on whether the second operand is included in the first operands' [prototype chain][prototype chain].
+To clarify, `instanceof` will return whether the first operand is an instance of second operand or one of its child classes.
+`instanceof` only works on objects.
+
+```javascript
+class Beverage {
+ // ...
+}
+
+// The Coffee class is a child of the Beverage class.
+class Coffee extends Beverage {
+ // ...
+}
+
+const java = new Coffee();
+
+java instanceof Coffee;
+// => true
+
+java instanceof Beverage;
+// => true
+```
+
+````exercism/advanced
+The `Array` class has a method called `Array.isArray()` that checks if its argument is an array.
+
+While `instanceof Array` will not work with an array created in a different realm such as an `iframe` in a webpage, `Array.isArray()` will.
+
+This is because the Array class has a different constructor in each realm, and each `iframe` has its own realm, meaning that the function in the prototype chain will be different, causing `instanceof Array` to fail.
+`Array.isArray()` is capable of ignoring this, and should always be used when possible.
+
+It can also survive false positives where an object isn't actually an `Array`, and merely has `Array` in its prototype chain.
+
+```javascript
+({ __proto__: Array.prototype }) instanceof Array
+// => true
+
+Array.isArray({ __proto__: Array.prototype })
+// => false
+```
+
+````
+
+## The `in` operator
+
+The `in` operator returns whether the first operand is a property of the second operand.
+It does not check that the property has a defined value.
+A property set to `undefined` will still be detected by `in`.
+
+```javascript
+class Coffee {
+ constructor() {
+ this.temperature = 'hot';
+ this.isDarkMatter = undefined;
+ }
+
+ coolDown() {
+ this.temperature = 'warm';
+ }
+}
+
+const espresso = new Coffee();
+
+'temperature' in espresso;
+// => true
+
+'color' in espresso;
+// => false
+
+'isDarkMatter' in espresso;
+// => true
+```
+
+````exercism/note
+`in` will return `true` for inherited properties and methods.
+
+```javascript
+"coolDown" in espresso
+// => true
+
+"constructor" in espresso
+// => true
+```
+
+To avoid this, use `Object.hasOwn()` instead
+````
+
+## The `Object.hasOwn()` function
+
+The `Object.hasOwn()` method returns whether the specified object _owns the given property_ (it is not inherited or a method).
+
+```javascript
+class Coffee {
+ constructor() {
+ this.temperature = 'hot';
+ }
+
+ coolDown() {
+ this.temperature = 'warm';
+ }
+}
+const cappuccino = new Coffee();
+
+Object.hasOwn(cappucino, 'temperature');
+// => true
+
+Object.hasOwn(cappucino, 'constructor');
+// => false
+
+Object.hasOwn(cappucino, 'coolDown');
+// => false
+```
+
+[primitives]: https://developer.mozilla.org/en-US/docs/Glossary/Primitive
+[typeof null is object]: https://2ality.com/2013/10/typeof-null.html
+[prototype chain]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Inheritance_and_the_prototype_chain
diff --git a/concepts/type-checking/introduction.md b/concepts/type-checking/introduction.md
new file mode 100644
index 0000000000..0afed179a3
--- /dev/null
+++ b/concepts/type-checking/introduction.md
@@ -0,0 +1,164 @@
+# About
+
+Knowning what the type of a piece of data is, is often very important for code to run smoothly and without errors.
+
+Javascript has several ways to check the type of a value or object.
+
+```exercism/note
+Javascript's type checking mechanisms can be somewhat unreliable.
+
+For better type safety and stronger types, you should probably use TypeScript, a language that builds on JavaScript, but with the type syntax of a static-typed language.
+```
+
+## The `typeof` operator
+
+The `typeof` operator returns the type of its operand.
+The output is a string matching the name of one of the [primitive data types][primitives], except for `"null"`.
+It can also be `"function"` or `"object"`.
+
+```javascript
+typeof undefined;
+// => "undefined"
+
+typeof true;
+// => "boolean"
+
+typeof 42;
+// => "number"
+
+typeof 'Hello, World!';
+// => "string"
+
+typeof function () {
+ return 'Hello, World';
+};
+// => "function"
+
+typeof [1, 2, 3, 4];
+// => "object"
+
+typeof { city: 'Stockholm', country: 'Sweden' };
+// => "object"
+```
+
+For [historical reasons][`typeof null` is `"object"`].
+
+## The `instanceof` operator
+
+For checking the type of an object, you can use the `instanceof` operator.
+It evaluates into a `boolean` depending on whether the second operand is included in the first operands' [prototype chain][prototype chain].
+To clarify, `instanceof` will return whether the first operand is an instance of second operand or one of its child classes.
+`instanceof` only works on objects.
+
+```javascript
+class Beverage {
+ // ...
+}
+
+// The Coffee class is a child of the Beverage class.
+class Coffee extends Beverage {
+ // ...
+}
+
+const java = new Coffee();
+
+java instanceof Coffee;
+// => true
+
+java instanceof Beverage;
+// => true
+```
+
+````exercism/advanced
+The `Array` class has a method called `Array.isArray()` that checks if its argument is an array.
+
+While `instanceof Array` will not work with an array created in a different realm such as an `iframe` in a webpage, `Array.isArray()` will.
+
+This is because the Array class has a different constructor in each realm, and each `iframe` has its own realm, meaning that the function in the prototype chain will be different, causing `instanceof Array` to fail.
+`Array.isArray()` is capable of ignoring this, and should always be used when possible.
+
+It can also survive false positives where an object isn't actually an `Array`, and merely has `Array` in its prototype chain.
+
+```javascript
+({ __proto__: Array.prototype }) instanceof Array
+// => true
+
+Array.isArray({ __proto__: Array.prototype })
+// => false
+```
+
+````
+
+## The `in` operator
+
+The `in` operator returns whether the first operand is a property of the second operand.
+It does not check that the property has a defined value.
+A property set to `undefined` will still be detected by `in`.
+
+```javascript
+class Coffee {
+ constructor() {
+ this.temperature = 'hot';
+ this.isDarkMatter = undefined;
+ }
+
+ coolDown() {
+ this.temperature = 'warm';
+ }
+}
+
+const espresso = new Coffee();
+
+'temperature' in espresso;
+// => true
+
+'color' in espresso;
+// => false
+
+'isDarkMatter' in espresso;
+// => true
+```
+
+````exercism/note
+`in` will return `true` for inherited properties and methods.
+
+```javascript
+"coolDown" in espresso
+// => true
+
+"constructor" in espresso
+// => true
+```
+
+To avoid this, use `Object.hasOwn()` instead
+````
+
+## The `Object.hasOwn()` function
+
+The `Object.hasOwn()` method returns whether the specified object _owns the given property_ (it is not inherited or a method).
+
+```javascript
+class Coffee {
+ constructor() {
+ this.temperature = 'hot';
+ }
+
+ coolDown() {
+ this.temperature = 'warm';
+ }
+}
+const cappuccino = new Coffee();
+
+Object.hasOwn(cappucino, 'temperature');
+// => true
+
+Object.hasOwn(cappucino, 'constructor');
+// => false
+
+Object.hasOwn(cappucino, 'coolDown');
+// => false
+```
+
+[primitives]: https://developer.mozilla.org/en-US/docs/Glossary/Primitive
+[typeof null is object]: https://2ality.com/2013/10/typeof-null.html
+[prototype chain]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Inheritance_and_the_prototype_chain
diff --git a/concepts/type-checking/links.json b/concepts/type-checking/links.json
new file mode 100644
index 0000000000..a8e4a31740
--- /dev/null
+++ b/concepts/type-checking/links.json
@@ -0,0 +1,14 @@
+[
+ {
+ "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof",
+ "description": "MDN: The typeof operator"
+ },
+ {
+ "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof",
+ "description": "MDN: The instanceof operator"
+ },
+ {
+ "url": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty",
+ "description": "MDN: The object.hasOwnProperty() method"
+ }
+]
diff --git a/config.json b/config.json
index 02da541c16..6fcb8932ad 100644
--- a/config.json
+++ b/config.json
@@ -16,13 +16,21 @@
"highlightjs_language": "javascript"
},
"test_runner": {
- "average_run_time": 5.0
+ "average_run_time": 5
},
"files": {
- "solution": ["%{kebab_slug}.js"],
- "test": ["%{kebab_slug}.spec.js"],
- "example": [".meta/proof.ci.js"],
- "exemplar": [".meta/exemplar.js"]
+ "solution": [
+ "%{kebab_slug}.js"
+ ],
+ "test": [
+ "%{kebab_slug}.spec.js"
+ ],
+ "example": [
+ ".meta/proof.ci.js"
+ ],
+ "exemplar": [
+ ".meta/exemplar.js"
+ ]
},
"exercises": {
"concept": [
@@ -30,7 +38,9 @@
"slug": "lasagna",
"name": "Lucian's Luscious Lasagna",
"uuid": "97bf898a-36fc-47fc-b870-01fc0c7fe554",
- "concepts": ["basics"],
+ "concepts": [
+ "basics"
+ ],
"prerequisites": [],
"status": "active"
},
@@ -38,63 +48,103 @@
"slug": "annalyns-infiltration",
"name": "Annalyn's Infiltration",
"uuid": "5acafcbb-20a0-45b9-b276-3d167e0de313",
- "concepts": ["booleans"],
- "prerequisites": ["basics"],
+ "concepts": [
+ "booleans"
+ ],
+ "prerequisites": [
+ "basics"
+ ],
"status": "beta"
},
{
"slug": "freelancer-rates",
"name": "Freelancer Rates",
"uuid": "0aff2fa7-55ea-47e9-af4a-78927d916baf",
- "concepts": ["numbers", "arithmetic-operators"],
- "prerequisites": ["basics"],
+ "concepts": [
+ "numbers",
+ "arithmetic-operators"
+ ],
+ "prerequisites": [
+ "basics"
+ ],
"status": "beta"
},
{
"slug": "poetry-club-door-policy",
"name": "Poetry Club Door Policy",
"uuid": "39549583-1889-490a-bf98-ffb2e0aefe44",
- "concepts": ["strings"],
- "prerequisites": ["basics"],
+ "concepts": [
+ "strings"
+ ],
+ "prerequisites": [
+ "basics"
+ ],
"status": "beta"
},
{
"slug": "elyses-enchantments",
"name": "Elyses Enchantments",
"uuid": "25cb0707-44f8-4800-b993-3fcb2b6d9f61",
- "concepts": ["arrays"],
- "prerequisites": ["numbers"],
+ "concepts": [
+ "arrays"
+ ],
+ "prerequisites": [
+ "numbers"
+ ],
"status": "beta"
},
{
"slug": "vehicle-purchase",
"name": "Vehicle Purchase",
"uuid": "f75a7e2c-053a-411e-8b3d-f978a2c5100e",
- "concepts": ["comparison", "conditionals"],
- "prerequisites": ["booleans", "numbers", "strings"],
+ "concepts": [
+ "comparison",
+ "conditionals"
+ ],
+ "prerequisites": [
+ "booleans",
+ "numbers",
+ "strings"
+ ],
"status": "beta"
},
{
"slug": "bird-watcher",
"name": "Bird Watcher",
"uuid": "3e648aa0-9ce7-4041-93d2-2b95f3832824",
- "concepts": ["increment-decrement", "for-loops"],
- "prerequisites": ["arrays", "comparison", "conditionals"],
+ "concepts": [
+ "increment-decrement",
+ "for-loops"
+ ],
+ "prerequisites": [
+ "arrays",
+ "comparison",
+ "conditionals"
+ ],
"status": "beta"
},
{
"slug": "mixed-juices",
"name": "Mixed Juices",
"uuid": "81c3fb86-af86-4c56-a45f-e021403c4070",
- "concepts": ["while-loops", "conditionals-switch"],
- "prerequisites": ["comparison", "conditionals", "arrays"],
+ "concepts": [
+ "while-loops",
+ "conditionals-switch"
+ ],
+ "prerequisites": [
+ "comparison",
+ "conditionals",
+ "arrays"
+ ],
"status": "beta"
},
{
"slug": "lucky-numbers",
"name": "Lucky Numbers",
"uuid": "78bbd7d2-b660-4791-a404-af5bfed31849",
- "concepts": ["type-conversion"],
+ "concepts": [
+ "type-conversion"
+ ],
"prerequisites": [
"booleans",
"strings",
@@ -108,26 +158,56 @@
"slug": "elyses-analytic-enchantments",
"name": "Elyses Analytic Enchantments",
"uuid": "45d956db-d4ef-4468-b1d3-47021f172c15",
- "concepts": ["arrow-functions", "array-analysis"],
- "prerequisites": ["arrays", "booleans", "callbacks", "numbers"],
+ "concepts": [
+ "arrow-functions",
+ "array-analysis"
+ ],
+ "prerequisites": [
+ "arrays",
+ "booleans",
+ "callbacks",
+ "numbers"
+ ],
"status": "beta"
},
{
"slug": "elyses-destructured-enchantments",
"name": "Elyses Destructured Enchantments",
"uuid": "d9b5cd13-2f2b-4034-a571-e66c847ed6f8",
- "concepts": ["array-destructuring", "rest-and-spread"],
- "prerequisites": ["arrays", "functions", "objects"],
+ "concepts": [
+ "array-destructuring",
+ "object-destructuring"
+ ],
+ "prerequisites": [
+ "arrays",
+ "functions",
+ "objects"
+ ],
+ "status": "beta"
+ },
+ {
+ "slug": "train-driver",
+ "name": "Train Driver",
+ "uuid": "6cef6712-cf1d-4b3e-9ace-1de3450b4285",
+ "concepts": [
+ "rest-and-spread"
+ ],
+ "prerequisites": [
+ "functions",
+ "array-destructuring",
+ "object-destructuring"
+ ],
"status": "beta"
},
{
"slug": "elyses-looping-enchantments",
"name": "Elyses Looping Enchantments",
"uuid": "e06f8f70-019f-4cec-924b-3971414e15d9",
- "concepts": ["array-loops"],
+ "concepts": [
+ "array-loops"
+ ],
"prerequisites": [
"arrays",
- "arrow-functions",
"callbacks",
"for-loops",
"conditionals"
@@ -146,60 +226,89 @@
"slug": "amusement-park",
"name": "Amusement Park",
"uuid": "2d40b46b-9e49-431c-913c-81d1f42f74df",
- "concepts": ["null-undefined"],
- "prerequisites": ["objects"],
+ "concepts": [
+ "null-undefined"
+ ],
+ "prerequisites": [
+ "objects"
+ ],
"status": "beta"
},
{
"slug": "pizza-order",
"name": "Pizza Order",
"uuid": "e9a9fa73-4497-43d5-a4ff-4eb319c98233",
- "concepts": ["recursion"],
+ "concepts": [
+ "recursion"
+ ],
"prerequisites": [
"functions",
"rest-and-spread",
"array-destructuring",
"array-transformations"
],
- "status": "wip"
+ "status": "beta"
},
{
"slug": "coordinate-transformation",
"name": "Coordinate Transformation",
"uuid": "5aa39e89-c601-4a66-ab72-5d8512d69e02",
- "concepts": ["closures"],
- "prerequisites": ["arrays", "functions"],
+ "concepts": [
+ "closures"
+ ],
+ "prerequisites": [
+ "arrays",
+ "functions"
+ ],
"status": "beta"
},
{
"slug": "fruit-picker",
"name": "Fruit Picker",
"uuid": "a6348db8-cc2b-4c53-9f43-3c23248d66f0",
- "concepts": ["callbacks"],
- "prerequisites": ["functions", "objects"],
+ "concepts": [
+ "callbacks"
+ ],
+ "prerequisites": [
+ "functions",
+ "objects"
+ ],
"status": "beta"
},
{
"slug": "translation-service",
"name": "Translation Service",
"uuid": "4a967656-8615-474e-a009-5c0b09f4386f",
- "concepts": ["promises"],
- "prerequisites": ["callbacks", "arrow-functions", "errors"],
+ "concepts": [
+ "promises"
+ ],
+ "prerequisites": [
+ "callbacks",
+ "arrow-functions",
+ "errors"
+ ],
"status": "beta"
},
{
"slug": "high-score-board",
"name": "High Score Board",
"uuid": "431d1f13-d280-4808-bbc6-a72e628c15c2",
- "concepts": ["objects"],
- "prerequisites": ["for-loops"],
+ "concepts": [
+ "objects"
+ ],
+ "prerequisites": [
+ "arithmetic-operators",
+ "for-loops"
+ ],
"status": "beta"
},
{
"slug": "ozans-playlist",
"name": "Ozan's Playlist",
"uuid": "347692fb-7b0f-4ef0-9a02-2192b59bdf5d",
- "concepts": ["sets"],
+ "concepts": [
+ "sets"
+ ],
"prerequisites": [
"array-destructuring",
"array-loops",
@@ -214,25 +323,39 @@
"slug": "lasagna-master",
"name": "Lasagna Master",
"uuid": "a7e323f1-84d8-43d5-8c26-cc119166b9fd",
- "concepts": ["functions"],
- "prerequisites": ["objects", "arrays", "null-undefined"],
+ "concepts": [
+ "functions"
+ ],
+ "prerequisites": [
+ "objects",
+ "arrays",
+ "null-undefined"
+ ],
"status": "beta"
},
{
"slug": "factory-sensors",
"name": "Factory Sensors",
"uuid": "2ccafa38-2802-44c1-8758-7415edefa909",
- "concepts": ["errors", "inheritance"],
- "prerequisites": ["classes", "null-undefined", "conditionals"],
+ "concepts": [
+ "errors",
+ "inheritance"
+ ],
+ "prerequisites": [
+ "classes",
+ "null-undefined",
+ "conditionals"
+ ],
"status": "beta"
},
{
"slug": "elyses-transformative-enchantments",
"name": "Elyses Transformative Enchantments",
"uuid": "6e156d67-2bd2-4624-956d-ddcc3795bad5",
- "concepts": ["array-transformations"],
+ "concepts": [
+ "array-transformations"
+ ],
"prerequisites": [
- "arrow-functions",
"numbers",
"arrays",
"conditionals",
@@ -244,17 +367,91 @@
"slug": "custom-signs",
"name": "Custom Signs",
"uuid": "02a9c753-614f-4814-a7a8-43c1971d2eb7",
- "concepts": ["conditionals-ternary", "template-strings"],
- "prerequisites": ["strings", "conditionals", "type-conversion"],
+ "concepts": [
+ "conditionals-ternary",
+ "template-strings"
+ ],
+ "prerequisites": [
+ "strings",
+ "conditionals",
+ "type-conversion"
+ ],
"status": "beta"
},
{
"slug": "windowing-system",
"name": "Windowing System",
"uuid": "d697850e-fd43-408c-a958-835aa8a510f7",
- "concepts": ["classes"],
- "prerequisites": ["objects", "functions", "conditionals-ternary"],
+ "concepts": [
+ "classes"
+ ],
+ "prerequisites": [
+ "objects",
+ "functions",
+ "conditionals-ternary"
+ ],
+ "status": "beta"
+ },
+ {
+ "slug": "regular-chatbot",
+ "name": "Regular Chatbot",
+ "uuid": "dc118a0d-be89-4ea5-a814-e471897d921a",
+ "concepts": [
+ "regular-expressions"
+ ],
+ "prerequisites": [
+ "arrays",
+ "classes",
+ "objects",
+ "template-strings"
+ ],
+ "status": "beta"
+ },
+ {
+ "slug": "appointment-time",
+ "name": "Appointment Time",
+ "uuid": "0f694053-a388-457f-89ca-f49be4560469",
+ "concepts": [
+ "dates"
+ ],
+ "prerequisites": [
+ "classes",
+ "objects",
+ "conditionals",
+ "type-conversion"
+ ],
"status": "beta"
+ },
+ {
+ "slug": "captains-log",
+ "name": "Captain's Log",
+ "uuid": "65cf28ab-243c-41cb-a720-f324f2cabe28",
+ "concepts": [
+ "randomness"
+ ],
+ "prerequisites": [
+ "numbers",
+ "arithmetic-operators",
+ "strings",
+ "objects",
+ "functions"
+ ]
+ },
+ {
+ "slug": "recycling-robot",
+ "name": "Recycling Robot",
+ "uuid": "16114449-52fe-470e-af11-cf9dc3689a93",
+ "concepts": [
+ "type-checking"
+ ],
+ "prerequisites": [
+ "basics",
+ "errors",
+ "objects",
+ "arrays",
+ "classes",
+ "inheritance"
+ ]
}
],
"practice": [
@@ -277,61 +474,114 @@
"name": "Two Fer",
"uuid": "7f49e997-4435-4f34-a020-bddc92c838ed",
"practices": [],
- "prerequisites": ["strings", "functions"],
+ "prerequisites": [
+ "strings",
+ "functions"
+ ],
"difficulty": 1,
- "topics": ["optional_values", "strings", "text_formatting"]
+ "topics": [
+ "optional_values",
+ "strings",
+ "text_formatting"
+ ]
},
{
"slug": "resistor-color",
"name": "Resistor Color",
"uuid": "53be6837-c224-45f1-bff3-d7f74d6285ce",
"practices": [],
- "prerequisites": ["arrays", "array-analysis"],
+ "prerequisites": [
+ "arrays",
+ "array-analysis"
+ ],
"difficulty": 1,
- "topics": ["arrays", "strings"]
+ "topics": [
+ "arrays",
+ "strings"
+ ]
},
{
"slug": "resistor-color-duo",
"name": "Resistor Color Duo",
"uuid": "de800041-3dcc-41b9-b101-7314ff685c93",
"practices": [],
- "prerequisites": ["array-analysis"],
+ "prerequisites": [
+ "array-analysis"
+ ],
"difficulty": 2,
- "topics": ["strings", "arrays"]
+ "topics": [
+ "strings",
+ "arrays"
+ ]
},
{
"slug": "gigasecond",
"name": "Gigasecond",
"uuid": "fd7b62d4-266b-4e84-a526-bf3d47901216",
"practices": [],
- "prerequisites": ["dates", "numbers", "arithmetic-operators"],
+ "prerequisites": [
+ "numbers",
+ "arithmetic-operators"
+ ],
"difficulty": 1,
- "topics": ["time"]
+ "topics": [
+ "time"
+ ]
+ },
+ {
+ "slug": "line-up",
+ "name": "Line Up",
+ "uuid": "034b31ef-331a-419d-bb62-465c05b533d8",
+ "practices": [
+ "numbers",
+ "strings"
+ ],
+ "prerequisites": [
+ "numbers",
+ "strings"
+ ],
+ "difficulty": 1
},
{
"slug": "rna-transcription",
- "name": "Rna Transcription",
+ "name": "RNA Transcription",
"uuid": "342974d6-9083-4754-a6c5-ed1e19e40ec5",
"practices": [],
- "prerequisites": ["strings", "array-transformations", "objects"],
+ "prerequisites": [
+ "strings",
+ "array-transformations",
+ "objects"
+ ],
"difficulty": 2,
- "topics": ["strings", "transforming"]
+ "topics": [
+ "strings",
+ "transforming"
+ ]
},
{
"slug": "space-age",
"name": "Space Age",
"uuid": "d9d757ed-ebe6-4d4a-aa73-f6834221cd54",
"practices": [],
- "prerequisites": ["objects", "numbers", "type-conversion"],
+ "prerequisites": [
+ "objects",
+ "numbers",
+ "type-conversion"
+ ],
"difficulty": 2,
- "topics": ["floating_point_numbers"]
+ "topics": [
+ "floating_point_numbers"
+ ]
},
{
"slug": "pangram",
"name": "Pangram",
"uuid": "da5b2b34-a1a7-4970-81f9-4665d875398b",
"practices": [],
- "prerequisites": ["strings", "array-analysis"],
+ "prerequisites": [
+ "strings",
+ "array-analysis"
+ ],
"difficulty": 2,
"topics": [
"algorithms",
@@ -368,7 +618,11 @@
"name": "Bob",
"uuid": "a5bf36f0-5d3c-41d4-8d54-e37e484e59cd",
"practices": [],
- "prerequisites": ["strings", "conditionals", "regular-expressions"],
+ "prerequisites": [
+ "strings",
+ "conditionals",
+ "regular-expressions"
+ ],
"difficulty": 4,
"topics": [
"conditionals",
@@ -391,14 +645,23 @@
"functions"
],
"difficulty": 5,
- "topics": ["algorithms", "conditionals", "loops", "lists", "sorting"]
+ "topics": [
+ "algorithms",
+ "conditionals",
+ "loops",
+ "lists",
+ "sorting"
+ ]
},
{
"slug": "pascals-triangle",
- "name": "Pascals Triangle",
+ "name": "Pascal's Triangle",
"uuid": "99493160-4673-402f-acda-62db5378148d",
"practices": [],
- "prerequisites": ["arrays", "for-loops"],
+ "prerequisites": [
+ "arrays",
+ "for-loops"
+ ],
"difficulty": 4,
"topics": [
"conditionals",
@@ -408,12 +671,27 @@
"text_formatting"
]
},
+ {
+ "slug": "split-second-stopwatch",
+ "name": "Split Second Stopwatch",
+ "uuid": "8ddc2921-c0f6-400e-bb74-c2bec51b9d63",
+ "practices": [],
+ "prerequisites": [
+ "classes",
+ "numbers"
+ ],
+ "difficulty": 4
+ },
{
"slug": "linked-list",
"name": "Linked List",
"uuid": "ec60a578-8889-46a1-b7b8-306dbd8551d5",
"practices": [],
- "prerequisites": ["classes", "conditionals", "while-loops"],
+ "prerequisites": [
+ "classes",
+ "conditionals",
+ "while-loops"
+ ],
"difficulty": 5,
"topics": [
"algorithms",
@@ -430,30 +708,46 @@
"name": "Grade School",
"uuid": "64637322-33bc-401f-8cec-1f9810a41f75",
"practices": [],
- "prerequisites": ["classes", "objects", "array-transformations"],
+ "prerequisites": [
+ "classes",
+ "objects",
+ "array-transformations"
+ ],
"difficulty": 5,
- "topics": ["arrays", "maps", "sorting"]
+ "topics": [
+ "arrays",
+ "maps",
+ "sorting"
+ ]
},
{
"slug": "list-ops",
"name": "List Ops",
"uuid": "7d9db056-5398-41b6-af3b-9707f5eb0dbc",
"practices": [],
- "prerequisites": ["classes", "arrays", "functions", "recursion"],
- "difficulty": 6,
- "topics": ["data_structures", "loops", "lists", "recursion"]
- },
- {
- "slug": "robot-name",
- "name": "Robot Name",
+ "prerequisites": [
+ "classes",
+ "arrays",
+ "functions",
+ "recursion"
+ ],
+ "difficulty": 6,
+ "topics": [
+ "data_structures",
+ "loops",
+ "lists",
+ "recursion"
+ ]
+ },
+ {
+ "slug": "robot-name",
+ "name": "Robot Name",
"uuid": "03f4dfea-e6db-4754-b2c8-ca06c8b81ef1",
"practices": [],
"prerequisites": [
"classes",
"arrays",
- "strings",
- "randomness",
- "codepoints"
+ "strings"
],
"difficulty": 6,
"topics": [
@@ -473,9 +767,7 @@
"strings",
"arrays",
"classes",
- "numbers",
- "codepoints",
- "randomness"
+ "numbers"
],
"difficulty": 6,
"topics": [
@@ -516,7 +808,10 @@
"name": "Secret Handshake",
"uuid": "74bbc9e3-edc5-41e0-84d7-5b2d98dd8370",
"practices": [],
- "prerequisites": ["bit-manipulation", "array-analysis", "errors"],
+ "prerequisites": [
+ "array-analysis",
+ "errors"
+ ],
"difficulty": 6,
"topics": [
"algorithms",
@@ -532,18 +827,31 @@
"name": "Leap",
"uuid": "193a0e19-462d-4d26-a117-124f07d5a3d7",
"practices": [],
- "prerequisites": ["arithmetic-operators", "booleans"],
+ "prerequisites": [
+ "arithmetic-operators",
+ "booleans"
+ ],
"difficulty": 1,
- "topics": ["booleans", "integers", "logic"]
+ "topics": [
+ "booleans",
+ "integers",
+ "logic"
+ ]
},
{
"slug": "reverse-string",
"name": "Reverse String",
"uuid": "e84c97eb-dbec-487c-b99f-ae9924e16293",
"practices": [],
- "prerequisites": ["strings", "array-transformations"],
+ "prerequisites": [
+ "strings",
+ "array-transformations"
+ ],
"difficulty": 2,
- "topics": ["loops", "strings"]
+ "topics": [
+ "loops",
+ "strings"
+ ]
},
{
"slug": "collatz-conjecture",
@@ -572,9 +880,29 @@
"name": "Triangle",
"uuid": "ed3ca73a-a0f0-46b8-8013-8b6d20758c8f",
"practices": [],
- "prerequisites": ["numbers", "booleans", "comparison", "classes"],
+ "prerequisites": [
+ "numbers",
+ "booleans",
+ "comparison",
+ "classes"
+ ],
"difficulty": 3,
- "topics": ["conditionals", "loops", "exception_handling", "integers"]
+ "topics": [
+ "conditionals",
+ "loops",
+ "exception_handling",
+ "integers"
+ ]
+ },
+ {
+ "slug": "camicia",
+ "name": "Camicia",
+ "uuid": "96d482ab-effc-4082-b4a8-3165de8ff0eb",
+ "practices": [],
+ "prerequisites": [
+ "arrays"
+ ],
+ "difficulty": 5
},
{
"slug": "clock",
@@ -588,14 +916,21 @@
"strings"
],
"difficulty": 5,
- "topics": ["dates", "globalization", "time"]
+ "topics": [
+ "dates",
+ "globalization",
+ "time"
+ ]
},
{
"slug": "meetup",
"name": "Meetup",
"uuid": "98617798-b49d-4d43-9f65-7131ee73d626",
"practices": [],
- "prerequisites": ["dates", "array-analysis", "conditionals-switch"],
+ "prerequisites": [
+ "array-analysis",
+ "conditionals-switch"
+ ],
"difficulty": 7,
"topics": [
"conditionals",
@@ -608,30 +943,58 @@
},
{
"slug": "etl",
- "name": "Etl",
+ "name": "ETL",
"uuid": "db16804b-0f63-445d-8beb-99e0f7218d66",
"practices": [],
- "prerequisites": ["objects", "array-loops", "arrays", "strings"],
+ "prerequisites": [
+ "objects",
+ "array-loops",
+ "arrays",
+ "strings"
+ ],
"difficulty": 2,
- "topics": ["loops", "integers", "maps", "transforming"]
+ "topics": [
+ "loops",
+ "integers",
+ "maps",
+ "transforming"
+ ]
},
{
"slug": "hamming",
"name": "Hamming",
"uuid": "d773c4ef-c09e-40e4-a7fe-01456cb4a12a",
"practices": [],
- "prerequisites": ["comparison", "errors", "for-loops"],
+ "prerequisites": [
+ "comparison",
+ "errors",
+ "for-loops"
+ ],
"difficulty": 2,
- "topics": ["conditionals", "loops", "equality", "strings"]
+ "topics": [
+ "conditionals",
+ "loops",
+ "equality",
+ "strings"
+ ]
},
{
"slug": "raindrops",
"name": "Raindrops",
"uuid": "f77ac2d1-cf3a-497d-bf04-b484a5a9cb37",
"practices": [],
- "prerequisites": ["arithmetic-operators", "strings", "conditionals"],
+ "prerequisites": [
+ "arithmetic-operators",
+ "strings",
+ "conditionals"
+ ],
"difficulty": 2,
- "topics": ["conditionals", "integers", "strings", "transforming"]
+ "topics": [
+ "conditionals",
+ "integers",
+ "strings",
+ "transforming"
+ ]
},
{
"slug": "nucleotide-count",
@@ -658,106 +1021,213 @@
"name": "Scrabble Score",
"uuid": "11771d47-1109-4579-a62b-e0b8e9583485",
"practices": [],
- "prerequisites": [],
+ "prerequisites": [
+ "strings",
+ "array-transformations"
+ ],
"difficulty": 5,
- "topics": ["conditionals", "loops", "maps", "strings"]
+ "topics": [
+ "conditionals",
+ "loops",
+ "maps",
+ "strings"
+ ]
},
{
"slug": "allergies",
"name": "Allergies",
"uuid": "9d33d21c-e695-427f-9f58-dd9498d61318",
"practices": [],
- "prerequisites": [],
+ "prerequisites": [
+ "classes",
+ "numbers",
+ "for-loops",
+ "arrays"
+ ],
"difficulty": 6,
- "topics": ["arrays", "bitwise_operations", "conditionals", "loops"]
+ "topics": [
+ "arrays",
+ "bitwise_operations",
+ "conditionals",
+ "loops"
+ ]
},
{
"slug": "word-count",
"name": "Word Count",
"uuid": "0073ff9a-cd6a-43cf-b8bf-4d5d8117b81b",
"practices": [],
- "prerequisites": [],
- "difficulty": 1,
- "topics": ["loops", "lists", "regular_expressions", "strings"]
+ "prerequisites": [
+ "strings",
+ "for-loops",
+ "regular-expressions",
+ "arrays",
+ "objects"
+ ],
+ "difficulty": 4,
+ "topics": [
+ "loops",
+ "lists",
+ "regular_expressions",
+ "strings"
+ ]
},
{
"slug": "bank-account",
"name": "Bank Account",
"uuid": "8beedf70-28b4-4cfb-ab53-309ee6f6aa78",
"practices": [],
- "prerequisites": [],
+ "prerequisites": [
+ "classes",
+ "conditionals",
+ "numbers"
+ ],
"difficulty": 3,
- "topics": ["classes", "conditionals"]
+ "topics": [
+ "classes",
+ "conditionals"
+ ]
},
{
"slug": "difference-of-squares",
- "name": "Difference Of Squares",
+ "name": "Difference of Squares",
"uuid": "7dfa878c-83a6-48ef-9170-b6633d51d601",
"practices": [],
- "prerequisites": ["classes", "for-loops"],
+ "prerequisites": [
+ "classes",
+ "for-loops"
+ ],
"difficulty": 3,
- "topics": ["algorithms", "loops", "integers", "math"]
+ "topics": [
+ "algorithms",
+ "loops",
+ "integers",
+ "math"
+ ]
},
{
"slug": "perfect-numbers",
"name": "Perfect Numbers",
"uuid": "c6691fd2-e10d-47df-acbf-3adeac5a2f89",
"practices": [],
- "prerequisites": [],
+ "prerequisites": [
+ "numbers",
+ "arrays",
+ "conditionals",
+ "errors",
+ "for-loops"
+ ],
"difficulty": 3,
- "topics": ["arrays", "conditionals", "loops", "integers", "math"]
+ "topics": [
+ "arrays",
+ "conditionals",
+ "loops",
+ "integers",
+ "math"
+ ]
},
{
"slug": "complex-numbers",
"name": "Complex Numbers",
"uuid": "ea9a9a3e-ae6a-470d-8bb4-2afead507f24",
"practices": [],
- "prerequisites": ["classes", "numbers", "math"],
+ "prerequisites": [
+ "classes",
+ "numbers"
+ ],
"difficulty": 4,
- "topics": ["math"]
+ "topics": [
+ "math"
+ ]
},
{
"slug": "luhn",
"name": "Luhn",
"uuid": "28872cc9-f1ef-487f-9a79-6bf7983148bf",
"practices": [],
- "prerequisites": [],
+ "prerequisites": [
+ "numbers",
+ "conditionals",
+ "array-transformations"
+ ],
"difficulty": 4,
- "topics": ["conditionals", "loops", "integers", "strings"]
+ "topics": [
+ "conditionals",
+ "loops",
+ "integers",
+ "strings"
+ ]
},
{
"slug": "prime-factors",
"name": "Prime Factors",
"uuid": "f43cdddf-eea8-4c4a-8359-c69e20ff9661",
"practices": [],
- "prerequisites": ["while-loops", "conditionals", "arrays"],
+ "prerequisites": [
+ "while-loops",
+ "conditionals",
+ "arrays"
+ ],
"difficulty": 4,
- "topics": ["algorithms", "conditionals", "loops", "integers", "math"]
+ "topics": [
+ "algorithms",
+ "conditionals",
+ "loops",
+ "integers",
+ "math"
+ ]
},
{
"slug": "grains",
"name": "Grains",
"uuid": "d003975a-9045-4f03-9ad9-c15db584dc13",
"practices": [],
- "prerequisites": [],
+ "prerequisites": [
+ "conditionals",
+ "errors",
+ "numbers"
+ ],
"difficulty": 5,
- "topics": ["loops", "integers"]
+ "topics": [
+ "loops",
+ "integers"
+ ]
},
{
"slug": "pythagorean-triplet",
"name": "Pythagorean Triplet",
"uuid": "394755a3-c743-4b85-b9b8-387907f4e32d",
"practices": [],
- "prerequisites": [],
+ "prerequisites": [
+ "classes",
+ "arrays",
+ "for-loops",
+ "numbers",
+ "conditionals"
+ ],
"difficulty": 5,
- "topics": ["algorithms", "conditionals", "loops", "integers", "math"]
+ "topics": [
+ "algorithms",
+ "conditionals",
+ "loops",
+ "integers",
+ "math"
+ ]
},
{
"slug": "palindrome-products",
"name": "Palindrome Products",
"uuid": "f6799d10-0210-4c73-ac08-d5cac1a00ff3",
"practices": [],
- "prerequisites": [],
+ "prerequisites": [
+ "classes",
+ "errors",
+ "strings",
+ "objects",
+ "for-loops",
+ "arrays",
+ "conditionals"
+ ],
"difficulty": 7,
"topics": [
"algorithms",
@@ -781,7 +1251,10 @@
"comparison"
],
"difficulty": 1,
- "topics": ["filtering", "strings"]
+ "topics": [
+ "filtering",
+ "strings"
+ ]
},
{
"slug": "acronym",
@@ -794,7 +1267,12 @@
"array-transformations"
],
"difficulty": 2,
- "topics": ["loops", "regular_expressions", "strings", "transforming"]
+ "topics": [
+ "loops",
+ "regular_expressions",
+ "strings",
+ "transforming"
+ ]
},
{
"slug": "high-scores",
@@ -808,24 +1286,38 @@
"classes"
],
"difficulty": 2,
- "topics": ["arrays"]
+ "topics": [
+ "arrays"
+ ]
},
{
"slug": "isogram",
"name": "Isogram",
"uuid": "3df577af-2854-40ee-b211-9b608dbbad58",
"practices": [],
- "prerequisites": ["strings", "arrays", "regular-expressions"],
+ "prerequisites": [
+ "strings",
+ "arrays",
+ "regular-expressions"
+ ],
"difficulty": 2,
- "topics": ["filtering", "strings"]
+ "topics": [
+ "filtering",
+ "strings"
+ ]
},
{
"slug": "matching-brackets",
"name": "Matching Brackets",
"uuid": "4d456646-3a9b-4393-9558-6b30e5c1039c",
"practices": [],
- "prerequisites": [],
- "difficulty": 3,
+ "prerequisites": [
+ "strings",
+ "arrays",
+ "conditionals-switch",
+ "for-loops"
+ ],
+ "difficulty": 3,
"topics": [
"conditionals",
"loops",
@@ -846,7 +1338,10 @@
"regular-expressions"
],
"difficulty": 3,
- "topics": ["parsing", "transforming"]
+ "topics": [
+ "parsing",
+ "transforming"
+ ]
},
{
"slug": "scale-generator",
@@ -855,23 +1350,47 @@
"practices": [],
"prerequisites": [],
"difficulty": 3,
- "topics": ["loops", "pattern_recognition", "strings", "arrays"]
+ "status": "deprecated",
+ "topics": [
+ "loops",
+ "pattern_recognition",
+ "strings",
+ "arrays"
+ ]
},
{
"slug": "series",
"name": "Series",
"uuid": "5178ae53-5364-46c9-bee3-70e6e8a8c2e3",
"practices": [],
- "prerequisites": [],
+ "prerequisites": [
+ "arrays",
+ "strings",
+ "errors",
+ "classes",
+ "conditionals",
+ "array-transformations"
+ ],
"difficulty": 3,
- "topics": ["loops", "exception_handling", "strings", "text_formatting"]
+ "topics": [
+ "loops",
+ "exception_handling",
+ "strings",
+ "text_formatting"
+ ]
},
{
"slug": "largest-series-product",
"name": "Largest Series Product",
"uuid": "1f84305d-ea76-4fe2-9858-3b53576d683d",
"practices": [],
- "prerequisites": [],
+ "prerequisites": [
+ "strings",
+ "numbers",
+ "errors",
+ "conditionals",
+ "array-transformations"
+ ],
"difficulty": 7,
"topics": [
"conditionals",
@@ -888,8 +1407,13 @@
"name": "Transpose",
"uuid": "9c140fb7-cc8b-411b-b613-a0e0081a9c3f",
"practices": [],
- "prerequisites": [],
- "difficulty": 1,
+ "prerequisites": [
+ "arrays",
+ "for-loops",
+ "strings",
+ "conditionals"
+ ],
+ "difficulty": 3,
"topics": [
"arrays",
"lists",
@@ -904,9 +1428,19 @@
"name": "Grep",
"uuid": "78bcbae1-a0f2-460c-ba89-e51844fe9397",
"practices": [],
- "prerequisites": [],
+ "prerequisites": [
+ "arrays",
+ "strings",
+ "for-loops",
+ "array-analysis",
+ "template-strings"
+ ],
"difficulty": 4,
- "topics": ["files", "searching", "text_formatting"]
+ "topics": [
+ "files",
+ "searching",
+ "text_formatting"
+ ]
},
{
"slug": "rectangles",
@@ -915,14 +1449,24 @@
"practices": [],
"prerequisites": [],
"difficulty": 4,
- "topics": ["arrays", "conditionals", "loops", "matrices", "strings"]
+ "topics": [
+ "arrays",
+ "conditionals",
+ "loops",
+ "matrices",
+ "strings"
+ ]
},
{
"slug": "spiral-matrix",
"name": "Spiral Matrix",
"uuid": "c1abafcc-0d44-4fb5-afae-bff3ce2e1b39",
"practices": [],
- "prerequisites": [],
+ "prerequisites": [
+ "arrays",
+ "for-loops",
+ "conditionals"
+ ],
"difficulty": 4,
"topics": [
"arrays",
@@ -934,7 +1478,7 @@
},
{
"slug": "ocr-numbers",
- "name": "Ocr Numbers",
+ "name": "OCR Numbers",
"uuid": "24c197ee-d492-4083-8615-629cb4b836b2",
"practices": [],
"prerequisites": [],
@@ -949,6 +1493,14 @@
"text_formatting"
]
},
+ {
+ "slug": "relative-distance",
+ "name": "Relative Distance",
+ "uuid": "c72faac5-8d41-404b-8558-759b94ea22ec",
+ "practices": [],
+ "prerequisites": [],
+ "difficulty": 5
+ },
{
"slug": "saddle-points",
"name": "Saddle Points",
@@ -974,7 +1526,11 @@
"practices": [],
"prerequisites": [],
"difficulty": 8,
- "topics": ["domain_specific_languages", "parsing", "stacks"]
+ "topics": [
+ "domain_specific_languages",
+ "parsing",
+ "stacks"
+ ]
},
{
"slug": "food-chain",
@@ -983,20 +1539,26 @@
"practices": [],
"prerequisites": [],
"difficulty": 4,
- "topics": ["algorithms", "text_formatting"]
+ "topics": [
+ "algorithms",
+ "text_formatting"
+ ]
},
{
"slug": "house",
"name": "House",
"uuid": "a8b7187d-12eb-4efc-b966-87823654ccda",
"practices": [],
- "prerequisites": [],
- "difficulty": 4,
- "topics": ["arrays", "conditionals", "loops", "recursion", "strings"]
+ "prerequisites": [
+ "for-loops",
+ "arrays",
+ "strings"
+ ],
+ "difficulty": 4
},
{
"slug": "isbn-verifier",
- "name": "Isbn Verifier",
+ "name": "ISBN Verifier",
"uuid": "b902c746-60d1-4645-a5bc-dafadec0ef32",
"practices": [],
"prerequisites": [],
@@ -1051,7 +1613,12 @@
"name": "Twelve Days",
"uuid": "64de1776-d5c6-43fe-9abf-7e3aa08ae342",
"practices": [],
- "prerequisites": ["arrays", "conditionals", "for-loops", "strings"],
+ "prerequisites": [
+ "arrays",
+ "conditionals",
+ "for-loops",
+ "strings"
+ ],
"difficulty": 4,
"topics": [
"conditionals",
@@ -1066,11 +1633,14 @@
"slug": "promises",
"name": "Promises",
"uuid": "ad21018c-e538-4ce5-a33a-949b4293df8c",
- "core": false,
"practices": [],
"prerequisites": [],
"difficulty": 6,
- "topics": ["promises", "error_handling", "higher_order_functions"]
+ "topics": [
+ "promises",
+ "error_handling",
+ "higher_order_functions"
+ ]
},
{
"slug": "yacht",
@@ -1085,21 +1655,26 @@
"sets"
],
"difficulty": 4,
- "topics": ["arrays", "conditionals", "filtering", "games"]
+ "topics": [
+ "arrays",
+ "conditionals",
+ "filtering",
+ "games"
+ ]
},
{
"slug": "beer-song",
"name": "Beer Song",
"uuid": "6573f168-d8fc-4ccf-a864-1a61f432fae1",
"practices": [],
- "prerequisites": [
- "arrays",
- "conditionals",
- "for-loops",
- "template-strings"
- ],
+ "prerequisites": [],
"difficulty": 5,
- "topics": ["conditionals", "loops", "strings"]
+ "status": "deprecated",
+ "topics": [
+ "conditionals",
+ "loops",
+ "strings"
+ ]
},
{
"slug": "resistor-color-trio",
@@ -1114,7 +1689,10 @@
"template-strings"
],
"difficulty": 5,
- "topics": ["conditionals", "loops"]
+ "topics": [
+ "conditionals",
+ "loops"
+ ]
},
{
"slug": "dominoes",
@@ -1130,7 +1708,9 @@
"while-loops"
],
"difficulty": 6,
- "topics": ["graph_theory"]
+ "topics": [
+ "graph_theory"
+ ]
},
{
"slug": "say",
@@ -1143,7 +1723,6 @@
"conditionals",
"errors",
"for-loops",
- "math",
"numbers",
"strings"
],
@@ -1162,7 +1741,11 @@
"name": "Diamond",
"uuid": "6a1eee0e-f8d4-446d-9c52-f31c3700af1b",
"practices": [],
- "prerequisites": ["arrays", "for-loops", "strings"],
+ "prerequisites": [
+ "arrays",
+ "for-loops",
+ "strings"
+ ],
"difficulty": 5,
"topics": [
"arrays",
@@ -1183,12 +1766,15 @@
"arithmetic-operators",
"classes",
"errors",
- "math",
"numbers",
"while-loops"
],
"difficulty": 5,
- "topics": ["algorithms", "floating_point_numbers", "math"]
+ "topics": [
+ "algorithms",
+ "floating_point_numbers",
+ "math"
+ ]
},
{
"slug": "sublist",
@@ -1202,16 +1788,28 @@
"conditionals"
],
"difficulty": 4,
- "topics": ["arrays", "lists"]
+ "topics": [
+ "arrays",
+ "lists"
+ ]
},
{
"slug": "binary-search-tree",
"name": "Binary Search Tree",
"uuid": "6c4b4e25-c115-4789-9058-d28ab6ca0d26",
"practices": [],
- "prerequisites": ["classes", "conditionals", "recursion"],
+ "prerequisites": [
+ "classes",
+ "conditionals",
+ "recursion"
+ ],
"difficulty": 6,
- "topics": ["algorithms", "conditionals", "loops", "recursion"]
+ "topics": [
+ "algorithms",
+ "conditionals",
+ "loops",
+ "recursion"
+ ]
},
{
"slug": "custom-set",
@@ -1247,11 +1845,16 @@
"comparison",
"conditionals",
"errors",
- "math",
"while-loops"
],
"difficulty": 7,
- "topics": ["algorithms", "arrays", "conditionals", "loops", "recursion"]
+ "topics": [
+ "algorithms",
+ "arrays",
+ "conditionals",
+ "loops",
+ "recursion"
+ ]
},
{
"slug": "circular-buffer",
@@ -1280,16 +1883,30 @@
"name": "Simple Linked List",
"uuid": "a1591026-2f02-45f9-b189-24b2359eb43f",
"practices": [],
- "prerequisites": ["arrays", "classes", "conditionals", "for-loops"],
+ "prerequisites": [
+ "arrays",
+ "classes",
+ "conditionals",
+ "for-loops"
+ ],
"difficulty": 8,
- "topics": ["arrays", "data_structures", "lists"]
+ "topics": [
+ "arrays",
+ "data_structures",
+ "lists"
+ ]
},
{
"slug": "word-search",
"name": "Word Search",
"uuid": "0125f5a7-8883-4553-a6c1-45f19544af5e",
"practices": [],
- "prerequisites": ["arrays", "array-loops", "classes", "conditionals"],
+ "prerequisites": [
+ "arrays",
+ "array-loops",
+ "classes",
+ "conditionals"
+ ],
"difficulty": 8,
"topics": [
"arrays",
@@ -1315,7 +1932,10 @@
"rest-and-spread"
],
"difficulty": 5,
- "topics": ["bitwise_operations", "transforming"]
+ "topics": [
+ "bitwise_operations",
+ "transforming"
+ ]
},
{
"slug": "two-bucket",
@@ -1357,7 +1977,10 @@
"rest-and-spread"
],
"difficulty": 7,
- "topics": ["algorithms", "games"]
+ "topics": [
+ "algorithms",
+ "games"
+ ]
},
{
"slug": "connect",
@@ -1424,8 +2047,13 @@
"practices": [],
"prerequisites": [],
"difficulty": 5,
- "topics": ["algorithms", "callbacks", "loops", "lists"],
- "status": "deprecated"
+ "status": "deprecated",
+ "topics": [
+ "algorithms",
+ "callbacks",
+ "loops",
+ "lists"
+ ]
},
{
"slug": "flatten-array",
@@ -1434,7 +2062,10 @@
"practices": [],
"prerequisites": [],
"difficulty": 5,
- "topics": ["arrays", "recursion"]
+ "topics": [
+ "arrays",
+ "recursion"
+ ]
},
{
"slug": "nth-prime",
@@ -1459,7 +2090,13 @@
"practices": [],
"prerequisites": [],
"difficulty": 5,
- "topics": ["conditionals", "loops", "integers", "math", "recursion"]
+ "topics": [
+ "conditionals",
+ "loops",
+ "integers",
+ "math",
+ "recursion"
+ ]
},
{
"slug": "rotational-cipher",
@@ -1468,11 +2105,16 @@
"practices": [],
"prerequisites": [],
"difficulty": 2,
- "topics": ["conditionals", "strings", "text_formatting", "transforming"]
+ "topics": [
+ "conditionals",
+ "strings",
+ "text_formatting",
+ "transforming"
+ ]
},
{
"slug": "diffie-hellman",
- "name": "Diffie Hellman",
+ "name": "Diffie-Hellman",
"uuid": "127eccbd-3009-4a8f-95c1-7d8aeb608550",
"practices": [],
"prerequisites": [],
@@ -1493,7 +2135,12 @@
"practices": [],
"prerequisites": [],
"difficulty": 4,
- "topics": ["algorithms", "arrays", "filtering", "math"]
+ "topics": [
+ "algorithms",
+ "arrays",
+ "filtering",
+ "math"
+ ]
},
{
"slug": "atbash-cipher",
@@ -1550,7 +2197,12 @@
"name": "Kindergarten Garden",
"uuid": "13444eff-005a-405e-9737-7b64d99c1a61",
"practices": [],
- "prerequisites": ["arrays", "classes", "strings", "rest-and-spread"],
+ "prerequisites": [
+ "arrays",
+ "classes",
+ "strings",
+ "rest-and-spread"
+ ],
"difficulty": 7,
"topics": [
"arrays",
@@ -1562,12 +2214,18 @@
},
{
"slug": "sum-of-multiples",
- "name": "Sum Of Multiples",
+ "name": "Sum of Multiples",
"uuid": "f7452f71-795b-40b6-847c-67ef4bb9db45",
"practices": [],
"prerequisites": [],
"difficulty": 5,
- "topics": ["conditionals", "loops", "integers", "lists", "math"]
+ "topics": [
+ "conditionals",
+ "loops",
+ "integers",
+ "lists",
+ "math"
+ ]
},
{
"slug": "change",
@@ -1576,7 +2234,11 @@
"practices": [],
"prerequisites": [],
"difficulty": 8,
- "topics": ["algorithms", "performance", "searching"]
+ "topics": [
+ "algorithms",
+ "performance",
+ "searching"
+ ]
},
{
"slug": "point-mutations",
@@ -1585,7 +2247,6 @@
"practices": [],
"prerequisites": [],
"difficulty": 1,
- "topics": null,
"status": "deprecated"
},
{
@@ -1595,7 +2256,12 @@
"practices": [],
"prerequisites": [],
"difficulty": 1,
- "topics": ["algorithms", "conditionals", "loops", "strings"]
+ "topics": [
+ "algorithms",
+ "conditionals",
+ "loops",
+ "strings"
+ ]
},
{
"slug": "armstrong-numbers",
@@ -1604,20 +2270,29 @@
"practices": [],
"prerequisites": [],
"difficulty": 2,
- "topics": ["algorithms", "math"]
+ "topics": [
+ "algorithms",
+ "math"
+ ]
},
{
"slug": "dnd-character",
"name": "D&D Character",
"uuid": "b373e13c-f179-4b36-b6e8-2a0f41540344",
"practices": [],
- "prerequisites": [],
+ "prerequisites": [
+ "randomness",
+ "classes"
+ ],
"difficulty": 2,
- "topics": ["classes", "randomness"]
+ "topics": [
+ "classes",
+ "randomness"
+ ]
},
{
"slug": "run-length-encoding",
- "name": "Run Length Encoding",
+ "name": "Run-Length Encoding",
"uuid": "6ac4ad5f-a64a-4646-91c5-169d53f289f9",
"practices": [],
"prerequisites": [],
@@ -1637,9 +2312,13 @@
"name": "Darts",
"uuid": "6c64649b-ea81-4118-9e74-a0a55018ffbc",
"practices": [],
- "prerequisites": [],
- "difficulty": 3,
- "topics": null
+ "prerequisites": [
+ "arithmetic-operators",
+ "comparison",
+ "conditionals",
+ "numbers"
+ ],
+ "difficulty": 3
},
{
"slug": "roman-numerals",
@@ -1662,6 +2341,7 @@
"practices": [],
"prerequisites": [],
"difficulty": 4,
+ "status": "deprecated",
"topics": [
"conditionals",
"loops",
@@ -1670,8 +2350,7 @@
"math",
"regular_expressions",
"strings"
- ],
- "status": "deprecated"
+ ]
},
{
"slug": "hexadecimal",
@@ -1680,6 +2359,7 @@
"practices": [],
"prerequisites": [],
"difficulty": 4,
+ "status": "deprecated",
"topics": [
"conditionals",
"loops",
@@ -1687,8 +2367,7 @@
"math",
"regular_expressions",
"strings"
- ],
- "status": "deprecated"
+ ]
},
{
"slug": "octal",
@@ -1697,6 +2376,7 @@
"practices": [],
"prerequisites": [],
"difficulty": 4,
+ "status": "deprecated",
"topics": [
"conditionals",
"loops",
@@ -1704,8 +2384,7 @@
"math",
"regular_expressions",
"strings"
- ],
- "status": "deprecated"
+ ]
},
{
"slug": "square-root",
@@ -1714,7 +2393,12 @@
"practices": [],
"prerequisites": [],
"difficulty": 4,
- "topics": ["bitwise_operations", "algorithms", "loops", "math"]
+ "topics": [
+ "bitwise_operations",
+ "algorithms",
+ "loops",
+ "math"
+ ]
},
{
"slug": "trinary",
@@ -1723,6 +2407,7 @@
"practices": [],
"prerequisites": [],
"difficulty": 4,
+ "status": "deprecated",
"topics": [
"conditionals",
"loops",
@@ -1730,8 +2415,7 @@
"math",
"regular_expressions",
"strings"
- ],
- "status": "deprecated"
+ ]
},
{
"slug": "all-your-base",
@@ -1749,6 +2433,19 @@
"parsing"
]
},
+ {
+ "slug": "flower-field",
+ "name": "Flower Field",
+ "uuid": "cb4da136-db03-44fa-a5c8-5235f273320c",
+ "practices": [],
+ "prerequisites": [],
+ "difficulty": 7,
+ "topics": [
+ "algorithms",
+ "arrays",
+ "games"
+ ]
+ },
{
"slug": "minesweeper",
"name": "Minesweeper",
@@ -1756,7 +2453,12 @@
"practices": [],
"prerequisites": [],
"difficulty": 7,
- "topics": ["algorithms", "arrays", "games"]
+ "status": "deprecated",
+ "topics": [
+ "algorithms",
+ "arrays",
+ "games"
+ ]
},
{
"slug": "queen-attack",
@@ -1764,7 +2466,7 @@
"uuid": "007a4cd4-7324-4512-8905-ead0c78146f7",
"practices": [],
"prerequisites": [],
- "difficulty": 8,
+ "difficulty": 4,
"topics": [
"conditionals",
"loops",
@@ -1782,7 +2484,11 @@
"practices": [],
"prerequisites": [],
"difficulty": 8,
- "topics": ["algorithms", "events", "reactive_programming"]
+ "topics": [
+ "algorithms",
+ "events",
+ "reactive_programming"
+ ]
},
{
"slug": "zipper",
@@ -1791,43 +2497,79 @@
"practices": [],
"prerequisites": [],
"difficulty": 8,
- "topics": ["recursion", "searching", "trees"]
+ "topics": [
+ "recursion",
+ "searching",
+ "trees"
+ ]
},
{
"slug": "zebra-puzzle",
"name": "Zebra Puzzle",
"uuid": "3a34cb49-546f-4aae-b49e-7d5b25db7ebb",
"practices": [],
- "prerequisites": ["arrays", "classes", "for-loops", "maps"],
+ "prerequisites": [
+ "arrays",
+ "classes",
+ "for-loops"
+ ],
"difficulty": 7,
- "topics": ["algorithms", "logic"]
+ "topics": [
+ "algorithms",
+ "logic"
+ ]
},
{
"slug": "tournament",
"name": "Tournament",
"uuid": "e5e5363d-4e9a-478f-8b18-e578f3721ac5",
"practices": [],
- "prerequisites": ["arrays", "strings", "for-loops", "objects"],
+ "prerequisites": [
+ "arrays",
+ "strings",
+ "for-loops",
+ "objects"
+ ],
"difficulty": 3,
- "topics": ["parsing", "strings"]
+ "topics": [
+ "parsing",
+ "strings"
+ ]
},
{
"slug": "rail-fence-cipher",
"name": "Rail Fence Cipher",
"uuid": "5f0352e5-0988-434a-82c7-f6810d41d63a",
"practices": [],
- "prerequisites": ["arrays", "strings", "for-loops", "conditionals"],
+ "prerequisites": [
+ "arrays",
+ "strings",
+ "for-loops",
+ "conditionals"
+ ],
"difficulty": 3,
- "topics": ["parsing", "strings", "iteration"]
+ "topics": [
+ "parsing",
+ "strings",
+ "iteration"
+ ]
},
{
"slug": "rest-api",
- "name": "Rest API",
+ "name": "REST API",
"uuid": "75b4816d-a5d7-4840-92d6-7249c9e8beeb",
"practices": [],
- "prerequisites": ["arrays", "classes", "array-loops", "objects"],
+ "prerequisites": [
+ "arrays",
+ "classes",
+ "array-loops",
+ "objects"
+ ],
"difficulty": 6,
- "topics": ["algorithms", "logic"]
+ "topics": [
+ "algorithms",
+ "logic"
+ ]
},
{
"slug": "go-counting",
@@ -1843,7 +2585,10 @@
"conditionals-switch"
],
"difficulty": 5,
- "topics": ["algorithms", "logic"]
+ "topics": [
+ "algorithms",
+ "logic"
+ ]
},
{
"slug": "knapsack",
@@ -1854,11 +2599,28 @@
"arrays",
"for-loops",
"objects",
- "math",
"conditionals"
],
"difficulty": 5,
- "topics": ["algorithms", "logic"]
+ "topics": [
+ "algorithms",
+ "logic"
+ ]
+ },
+ {
+ "slug": "prism",
+ "name": "Prism",
+ "uuid": "ef463b82-bf5c-4761-a821-29eeabee3050",
+ "practices": [],
+ "prerequisites": [
+ "arithmetic-operators",
+ "arrays",
+ "array-loops",
+ "comparison",
+ "conditionals",
+ "for-loops"
+ ],
+ "difficulty": 5
},
{
"slug": "satellite",
@@ -1872,8 +2634,7 @@
"conditionals",
"recursion"
],
- "difficulty": 6,
- "topics": []
+ "difficulty": 6
},
{
"slug": "poker",
@@ -1888,8 +2649,135 @@
"conditionals",
"strings"
],
+ "difficulty": 7
+ },
+ {
+ "slug": "eliuds-eggs",
+ "name": "Eliud's Eggs",
+ "uuid": "2a3ecf62-fb5d-4dac-8369-72e7976a8f57",
+ "practices": [],
+ "prerequisites": [
+ "strings",
+ "regular-expressions",
+ "rest-and-spread",
+ "arrow-functions",
+ "basics"
+ ],
+ "difficulty": 2
+ },
+ {
+ "slug": "parallel-letter-frequency",
+ "name": "Parallel Letter Frequency",
+ "uuid": "63126f78-ba0d-4271-978d-49e9312f0db2",
+ "practices": [],
+ "prerequisites": [
+ "strings",
+ "regular-expressions",
+ "rest-and-spread",
+ "arrow-functions",
+ "basics"
+ ],
+ "difficulty": 7
+ },
+ {
+ "slug": "bottle-song",
+ "name": "Bottle Song",
+ "uuid": "2da8329c-2a92-4232-b104-2a08a76cab60",
+ "practices": [],
+ "prerequisites": [
+ "arrays",
+ "conditionals",
+ "for-loops",
+ "template-strings"
+ ],
+ "difficulty": 5,
+ "topics": [
+ "conditionals",
+ "loops",
+ "strings"
+ ]
+ },
+ {
+ "slug": "markdown",
+ "name": "Markdown",
+ "uuid": "cd666b3a-7114-4ba9-9b2a-7622a2c8c12c",
+ "practices": [],
+ "prerequisites": [],
+ "difficulty": 5
+ },
+ {
+ "slug": "micro-blog",
+ "name": "Micro Blog",
+ "uuid": "ee771d09-33fb-4450-b9c3-d591a4a90a99",
+ "practices": [],
+ "prerequisites": [
+ "strings"
+ ],
+ "difficulty": 2
+ },
+ {
+ "slug": "ledger",
+ "name": "Ledger",
+ "uuid": "8716b347-e18f-48a6-b373-426cc4ca98cb",
+ "practices": [],
+ "prerequisites": [],
+ "difficulty": 5
+ },
+ {
+ "slug": "state-of-tic-tac-toe",
+ "name": "State of Tic-Tac-Toe",
+ "uuid": "b381543d-2bcf-4999-9d44-788a2b39c1a0",
+ "practices": [],
+ "prerequisites": [
+ "strings",
+ "array-loops",
+ "conditionals",
+ "errors"
+ ],
+ "difficulty": 5
+ },
+ {
+ "slug": "lens-person",
+ "name": "Lens Person",
+ "uuid": "a1e71425-0e7e-442a-9c8e-cc252f440760",
+ "practices": [],
+ "prerequisites": [
+ "callbacks"
+ ],
"difficulty": 7,
- "topics": []
+ "topics": [
+ "lens",
+ "classes",
+ "callbacks"
+ ]
+ },
+ {
+ "slug": "killer-sudoku-helper",
+ "name": "Killer Sudoku Helper",
+ "uuid": "4b6b00cd-62f8-4e9e-b59d-79f153f8efb5",
+ "practices": [],
+ "prerequisites": [
+ "conditionals",
+ "recursion",
+ "numbers"
+ ],
+ "difficulty": 5
+ },
+ {
+ "slug": "game-of-life",
+ "name": "Conway's Game of Life",
+ "uuid": "e51c01e9-b7b1-4877-939a-6254c4efe338",
+ "practices": [],
+ "prerequisites": [],
+ "difficulty": 2
+ },
+ {
+ "slug": "error-handling",
+ "name": "Error Handling",
+ "uuid": "de1c75f2-2461-4347-b5ca-b3cfaafe4d79",
+ "practices": [],
+ "prerequisites": [],
+ "difficulty": 4
}
]
},
@@ -1899,11 +2787,6 @@
"slug": "arithmetic-operators",
"name": "Arithmetic Operators"
},
- {
- "uuid": "e1b15569-387c-4833-8c3b-9a94e0ee1583",
- "slug": "array-analysis",
- "name": "Array Analysis"
- },
{
"uuid": "9f8f96bb-db13-485a-bfe4-6ae3fe2fbf46",
"slug": "array-destructuring",
@@ -1924,11 +2807,6 @@
"slug": "arrays",
"name": "Arrays"
},
- {
- "uuid": "e7eea65d-5a13-44ee-aae6-113cfb234457",
- "slug": "arrow-functions",
- "name": "Arrow Functions"
- },
{
"uuid": "611d6b3d-1241-4432-90f6-8fcffb36917c",
"slug": "basics",
@@ -1949,6 +2827,11 @@
"slug": "closures",
"name": "Closures"
},
+ {
+ "uuid": "ef1e44db-5fd0-4550-9135-7522303e7e42",
+ "slug": "comparison",
+ "name": "Comparison"
+ },
{
"uuid": "2d0b9f1f-c135-4014-b87c-25b081387002",
"slug": "conditionals",
@@ -1964,6 +2847,26 @@
"slug": "errors",
"name": "Errors"
},
+ {
+ "uuid": "be301f37-6cf4-4688-949e-75a8976a91e3",
+ "slug": "for-loops",
+ "name": "For Loops"
+ },
+ {
+ "uuid": "66264917-2d3e-42e9-806c-0fa740852f0f",
+ "slug": "functions",
+ "name": "Functions"
+ },
+ {
+ "uuid": "a2347a19-1e44-449b-9741-94eda00d8ba7",
+ "slug": "increment-decrement",
+ "name": "Increment/Decrement"
+ },
+ {
+ "uuid": "c890e216-5acb-4fb8-8081-61eb78eabe87",
+ "slug": "inheritance",
+ "name": "Inheritance"
+ },
{
"uuid": "008f1c88-7c14-48b2-a88d-49ecb5e3b122",
"slug": "null-undefined",
@@ -1974,15 +2877,25 @@
"slug": "numbers",
"name": "Numbers"
},
+ {
+ "uuid": "68dd89d3-13b8-49f5-b493-ccfdc82a073c",
+ "slug": "objects",
+ "name": "Objects"
+ },
+ {
+ "uuid": "38bb937b-b9d6-4550-8337-5e4f2623837a",
+ "slug": "object-destructuring",
+ "name": "Object Destructuring"
+ },
{
"uuid": "b3aa57d9-74b2-4d04-a673-ae2402630d8b",
"slug": "promises",
"name": "Promises"
},
{
- "uuid": "cfbc96fa-717e-4f29-a91d-760ebea88822",
- "slug": "recursion",
- "name": "Recursion"
+ "uuid": "e0ff7a2b-d6f7-4513-9589-3e6854e14415",
+ "slug": "classes",
+ "name": "Prototypes & Classes"
},
{
"uuid": "efc895b2-8420-44f1-a385-5c637286f797",
@@ -1990,34 +2903,34 @@
"name": "Rest and Spread"
},
{
- "uuid": "19085ad2-038a-4e08-ad34-47ff2a78fec6",
- "slug": "template-strings",
- "name": "Template Strings"
+ "uuid": "e7eea65d-5a13-44ee-aae6-113cfb234457",
+ "slug": "arrow-functions",
+ "name": "Arrow Functions"
},
{
- "uuid": "7d5c1533-c7cf-418e-b0f2-080da1e5bdc5",
- "slug": "strings",
- "name": "Strings"
+ "uuid": "e1b15569-387c-4833-8c3b-9a94e0ee1583",
+ "slug": "array-analysis",
+ "name": "Array Analysis"
},
{
- "uuid": "ef1e44db-5fd0-4550-9135-7522303e7e42",
- "slug": "comparison",
- "name": "Comparison"
+ "uuid": "cfbc96fa-717e-4f29-a91d-760ebea88822",
+ "slug": "recursion",
+ "name": "Recursion"
},
{
- "uuid": "a2347a19-1e44-449b-9741-94eda00d8ba7",
- "slug": "increment-decrement",
- "name": "Increment/Decrement"
+ "uuid": "e5695bba-50c1-4c55-af3e-2444883bd83b",
+ "slug": "regular-expressions",
+ "name": "Regular Expressions"
},
{
- "uuid": "be301f37-6cf4-4688-949e-75a8976a91e3",
- "slug": "for-loops",
- "name": "For Loops"
+ "uuid": "84e55c29-d403-4a90-8a2a-9960feae8ff3",
+ "slug": "sets",
+ "name": "Sets"
},
{
- "uuid": "4e68e39a-e36c-4d2d-8714-eb6482e31ff5",
- "slug": "while-loops",
- "name": "While Loops"
+ "uuid": "7d5c1533-c7cf-418e-b0f2-080da1e5bdc5",
+ "slug": "strings",
+ "name": "Strings"
},
{
"uuid": "d5d54931-b2a7-4b1a-a593-ad85a2810f2f",
@@ -2025,19 +2938,14 @@
"name": "Switch Statement"
},
{
- "uuid": "68dd89d3-13b8-49f5-b493-ccfdc82a073c",
- "slug": "objects",
- "name": "Objects"
- },
- {
- "uuid": "84e55c29-d403-4a90-8a2a-9960feae8ff3",
- "slug": "sets",
- "name": "Sets"
+ "uuid": "19085ad2-038a-4e08-ad34-47ff2a78fec6",
+ "slug": "template-strings",
+ "name": "Template Strings"
},
{
- "uuid": "66264917-2d3e-42e9-806c-0fa740852f0f",
- "slug": "functions",
- "name": "Functions"
+ "uuid": "168cb8e8-c4f9-4e10-9d79-bffc77b86bbf",
+ "slug": "conditionals-ternary",
+ "name": "Ternary Operator"
},
{
"uuid": "cbad4d23-a9d8-4370-add2-f4416a4df027",
@@ -2045,67 +2953,67 @@
"name": "Type Conversion"
},
{
- "uuid": "168cb8e8-c4f9-4e10-9d79-bffc77b86bbf",
- "slug": "conditionals-ternary",
- "name": "Ternary Operator"
+ "uuid": "4e68e39a-e36c-4d2d-8714-eb6482e31ff5",
+ "slug": "while-loops",
+ "name": "While Loops"
},
{
- "uuid": "c890e216-5acb-4fb8-8081-61eb78eabe87",
- "slug": "inheritance",
- "name": "Inheritance"
+ "uuid": "ca322d6f-0f7e-4a2d-a058-e98a59cdae93",
+ "slug": "randomness",
+ "name": "Randomness"
},
{
- "uuid": "e0ff7a2b-d6f7-4513-9589-3e6854e14415",
- "slug": "classes",
- "name": "Prototypes & Classes"
+ "uuid": "72e51fbe-db98-492e-b155-8ef21623f741",
+ "slug": "type-checking",
+ "name": "Type Checking"
}
],
"key_features": [
{
- "icon": "cross-platform",
"title": "Runs almost everywhere",
- "content": "Build web-pages, write backend, create database scripts, make mobile apps, design CLI-s, and more."
+ "content": "Build web-pages, write backend, create database scripts, make mobile apps, design CLI-s, and more.",
+ "icon": "cross-platform"
},
{
- "icon": "multi-paradigm",
"title": "Use any programming style",
- "content": "Use prototype-based, object-oriented, functional, or declarative programming styles, and more."
+ "content": "Use prototype-based, object-oriented, functional, or declarative programming styles, and more.",
+ "icon": "multi-paradigm"
},
{
- "icon": "dynamically-typed",
"title": "No types required",
- "content": "Dynamically and weakly typed by default, gain typed confidence using Flow, JSDoc, or TypeScript."
+ "content": "Dynamically and weakly typed by default, gain typed confidence using Flow, JSDoc, or TypeScript.",
+ "icon": "dynamically-typed"
},
{
- "icon": "concurrency",
"title": "Concurrency is safe",
- "content": "Async/await, dedicated workers, or state sync in shared workers. No deadlocks or race conditions."
+ "content": "Async/await, dedicated workers, or state sync in shared workers. No deadlocks or race conditions.",
+ "icon": "concurrency"
},
{
- "icon": "tooling",
"title": "Largest package registry",
- "content": "No need to reinvent the wheel. Build on top of > 1.3 million packages (April, 2020)."
+ "content": "No need to reinvent the wheel. Build on top of > 1.3 million packages (April, 2020).",
+ "icon": "tooling"
},
{
- "icon": "community",
"title": "Designed by a committee",
- "content": "Frequent updates, following ECMAScript, a general purpose, cross platform, vendor-neutral standard."
+ "content": "Frequent updates, following ECMAScript, a general purpose, cross platform, vendor-neutral standard.",
+ "icon": "community"
}
],
"tags": [
+ "execution_mode/interpreted",
"paradigm/declarative",
"paradigm/functional",
"paradigm/imperative",
"paradigm/object_oriented",
- "typing/dynamic",
- "typing/weak",
- "execution_mode/interpreted",
- "platform/windows",
- "platform/mac",
- "platform/linux",
- "platform/ios",
"platform/android",
+ "platform/ios",
+ "platform/linux",
+ "platform/mac",
"platform/web",
+ "platform/windows",
+ "typing/dynamic",
+ "typing/weak",
"used_for/artificial_intelligence",
"used_for/backends",
"used_for/cross_platform_development",
diff --git a/config/exercise-readme-insert.md b/config/exercise-readme-insert.md
index d2fa14273b..7927d0366a 100644
--- a/config/exercise-readme-insert.md
+++ b/config/exercise-readme-insert.md
@@ -14,7 +14,13 @@ Please `cd` into exercise directory before running all below commands.
Install assignment dependencies:
```bash
-$ npm install
+$ corepack pnpm install
+```
+
+If `corepack` complains about not being enabled, you can do so by running:
+
+```bash
+corepack enable pnpm
```
## Making the test suite pass
@@ -22,7 +28,7 @@ $ npm install
Execute the tests with:
```bash
-$ npm test
+$ corepack pnpm test
```
In the test suites all tests but the first have been skipped.
diff --git a/docs/FUNCTIONS.md b/docs/FUNCTIONS.md
new file mode 100644
index 0000000000..a313df3c18
--- /dev/null
+++ b/docs/FUNCTIONS.md
@@ -0,0 +1,195 @@
+# Functions
+
+In JavaScript, functions are _first class objects_ which means they can be assigned to variables and properties, be passed to other functions, and returned from functions. They can have properties and methods, just like any other _object_.
+
+## Input: passing arguments
+
+JavaScript functions allow you to pass arguments when they are called. Arguments are always [passed by value](https://dev.to/xpbytes/javascript-ruby-and-c-are-not-call-by-reference-23f7). In other words, if a function reassigns a parameter inside the function, the value won't change outside the function. That doesn't mean that passed-in objects cannot be mutated.
+
+> [!TIP]
+> When an object is passed in, the reference to that object is passed in by value. This makes it _seem_ like passing in objects are handled differently, but that's not the case.
+
+An example of a function that takes two arguments and multiplies them is:
+
+```javascript
+function multiply(a, b) {
+ return a * b;
+}
+
+multiply(2, 3);
+// => 6
+```
+
+## Output: return value
+
+JavaScript can return a single value from a function by using the `return` keyword, followed by an expression. When nothing is returned from a function, the returned value is `undefined`.
+
+An example of a function that returns a value is:
+
+```javascript
+function multiply(a, b) {
+ return a * b;
+}
+
+multiply(2, 3);
+// => 6
+```
+
+## Defining functions
+
+There are four flavours of functions in JavaScript:
+
+1. Regular functions that can return anything and always run to completion;
+2. Async functions that always return a `Promise`, and can paused and resumed with the `await` operator;
+3. Generator functions: that always return a `Generator` object, and can be paused and resumed with the `yield` operator;
+4. Async generator functions; that always return an `AsyncGenerator` object, and can be paused and resumed with _both_ the `yield` and `await` operators.
+
+For each of these four flavours, there are three ways to define them:
+
+1. using a declaration
+2. using an expression
+3. using the constructor
+
+Finally there is some special syntax to define _arrow functions and methods_.
+
+## Supported definitions
+
+The tooling used by Exercism to give automated feedback recognizes the following types of declarations and expressions. Definitions using the constructors such as ` Function()` are not supported.
+
+```javascript
+function declaration() { return 42 }
+function* declaration() { yield 42 }
+async function declaration() { }
+async function* declaration() { }
+
+const named = function () { return 42 }
+const named = function* () { return 42 }
+const named = async function() { }
+const named = async function*() { }
+let named = ...
+var name = ...
+
+const arrow = () => { return 42 }
+const arrow = () => 42
+const arrow = async () => { }
+const arrow = async () => 42
+
+collection.assignment = () => {}
+collection.assignment = async () => {}
+
+Object.defineProperty(collection, 'property', { value: () => { return 42 } })
+
+const computed = 'name'
+const collection = {
+ shorthand() { return 42 },
+ *shorthand() { yield 42 },
+ async shorthand() { },
+ async *shorthand() { }
+
+ [computed]() { return 42 }
+ *[computed]() { yield 42 }
+ async [computed]() { }
+ async *[computed]() { }
+
+ property: () => { return 42 },
+ property: async () => {},
+ property: function () { return 42 },
+ property: function* () { yield 42 },
+ property: async function () {},
+ property: async function* () {},
+
+ [computed]: () => { return 42 },
+ [computed]: async () => {},
+ [computed]: function () { return 42 },
+ [computed]: function* () { yield 42 },
+ [computed]: async function () {},
+ [computed]: async function* () {}
+}
+
+class Klazz {
+ get property() { return 42 }
+ set property(value) { }
+
+ property = () => { return 42 }
+ property = async () => {}
+ property = function () { return 42 }
+ property = function* () { yield 42 }
+ property = async function () { }
+ property = async function* () { }
+
+ [computed] = () => { return 42 },
+ [computed] = async () => {},
+ [computed] = function () { return 42 },
+ [computed] = function* () { yield 42 },
+ [computed] = async function () {},
+ [computed] = async function* () {}
+
+ shorthand() { return 42 }
+ *shorthand() { return 42 }
+ async shorthand() { }
+ async *shorthand() { }
+
+ [computed]() { return 42 }
+ *[computed]() { yield 42 }
+ async [computed]() { }
+ async *[computed]() { }
+
+ static property = () => { return 42 }
+ static property = async () => {}
+ static property = function () { return 42 }
+ static property = function* () { yield 42 }
+ static property = async function () { }
+ static property = async function* () { }
+
+ static [computed] = () => { return 42 }
+ static [computed] = async () => {}
+ static [computed] = function () { return 42 }
+ static [computed] = function* () { yield 42 }
+ static [computed] = async function () { }
+ static [computed] = async function* () { }
+
+ static shorthand() { return 42 }
+ static *shorthand() { yield 42 }
+ static async shorthand() {}
+ static async *shorthand() {}
+
+ static [computed]() { return 42 }
+ static *[computed]() { yield 42 }
+ static async [computed]() { }
+ static async *[computed]() { }
+
+ #property = () => { }
+ #shorthand() { }
+}
+
+Klazz.prototype.fn = () => { }
+Klazz.prototype.fn = async () => { }
+Klazz.prototype.fn = function () { }
+Klazz.prototype.fn = function* () { }
+Klazz.prototype.fn = async function () { }
+Klazz.prototype.fn = async function* () { }
+
+export default { name: () => {} }
+export default { name: async () => {} }
+export default { name: function () {} }
+export default { name: function* () {} }
+export default { name: async function () {} }
+export default { name: async function* () {} }
+
+export default { [computed]: () => {} }
+export default { [computed]: async () => {} }
+export default { [computed]: function () {} }
+export default { [computed]: function* () {} }
+export default { [computed]: async function () {} }
+export default { [computed]: async function* () {} }
+
+export default { name() {} }
+export default { *name() {} }
+export default { async name() {} }
+export default { async *name() {} }
+
+export default { [computed]() {} }
+export default { *[computed]() {} }
+export default { async [computed]() {} }
+export default { async *[computed]() {} }
+```
diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md
index 0767f1b780..ee9a3f4818 100644
--- a/docs/INSTALLATION.md
+++ b/docs/INSTALLATION.md
@@ -48,11 +48,12 @@ After the installer is done, or the package manager has completed, or the binary
The version should match the one on the website.
-**Note**: It is important to open a _new_ terminal window.
-Any open terminal windows might not have been refreshed after the installation completed.
-This means that the open terminals don't know that a new program was installed.
+> [!NOTE]
+> It is important to open a _new_ terminal window.
+> Any open terminal windows might not have been refreshed after the installation completed.
+> This means that the open terminals don't know that a new program was installed.
-> _**Help**_: `'node' is not recognised`
+> [!IMPORTANT] > _**Help**_: `'node' is not recognised`
>
> If you've used the official installer, your `PATH` should have been automatically configured, but if your shell has trouble locating your globally installed modules — or if you build Node.js from source — update your `PATH` to include the `npm` binaries.
>
@@ -69,6 +70,14 @@ This means that the open terminals don't know that a new program was installed.
>
> Close any open terminals and open a new one.
+## Enabling corepack
+
+In order to use a versioned package manager compatible with this track, `corepack` needs to be enabled once:
+
+```shell
+corepack enable pnpm
+```
+
## Assignment Requirements
Please follow [these instructions][cli-walkthrough] to download the Exercism CLI for your OS.
@@ -83,26 +92,23 @@ Each assignment then needs some tools to run the tests.
They can be installed running this command within each assignment directory:
```shell
-npm install
+corepack pnpm install
```
-> _**Help**_: `'' is missing / cannot be found`
->
-> If you see this after _upgrading_ your exercise, welcome to npm 7.
-> Delete `node_modules` and `package-lock.json` and re-run the command to resolve this.
-
-If you're concerned about disk space and are okay installing another tool, take a look at [pnpm](https://pnpm.io/), which ensure only one copy of each package-version is ever installed on disk.
-In this case, run `pnpm install` instead of `npm install`, and everything should work as expected.
+As this track has switched to pnpm, you should not be concerned about disk space.
+Take a look at [pnpm](https://pnpm.io/), which ensures only one copy of each package-version is ever installed on disk.
-> **But what is npm and why does this work?**
+> **But what is corepack and why does this work?**
>
> You don't need this information to complete the JavaScript track, but if you're eager to understand what just happened, the following paragraphs are for you:
>
-> This works because `npm` is a package manager that comes bundled with Node.js, which has been installed per the steps above.
-> The `npm` command looks for a `package.json` file, which is present in _each_ assignment folder.
-> This file lists the `"dependencies"` above, which are then downloaded by `npm` and placed into the `node_modules` folder.
+> This works because `corepack` is a tool that comes bundled with Node.js, which has been installed per the steps above.
+> It can install the package manager `pnpm`. It configures the system
+> The `corepack` command looks for a `package.json` file, which is present in _each_ assignment folder.
+> It then checks `packageManager` which matches `pnpm`, so it may continue. If necessary it will upgrade `pnpm` first.
+> This file also lists the `"dependencies"` above, which are then downloaded by `pnpm` and placed into a local cache.
>
-> The scripts in the `package.json` use the binaries from the local `node_modules` folder, and it's these scripts that are used to run the tests, as listed in the `exercise` description.
+> The scripts in the `package.json` use the binaries from the local cache, and it's these scripts that are used to run the tests, as listed in the `exercise` description.
[web-nodejs]: https://nodejs.org/
[web-nodejs-download]: https://nodejs.org/en/download/
diff --git a/docs/REPRESENTER_NORMALIZATIONS.md b/docs/REPRESENTER_NORMALIZATIONS.md
new file mode 100644
index 0000000000..152d6d2c81
--- /dev/null
+++ b/docs/REPRESENTER_NORMALIZATIONS.md
@@ -0,0 +1,85 @@
+# Representer normalizations
+
+The [JavaScript representer][github-javascript-representer] applies the following normalizations:
+
+## Remove comments
+
+All comments are removed.
+The following examples are equivalent:
+
+### Example with comments
+
+```javascript
+/**
+ * Returns the string literal 'hi there' just to say hello
+ * @return [String]
+ */
+function hello(/* nothing*/) {
+ // just return it
+ return 'hi there';
+}
+```
+
+### Example without comments
+
+```javascript
+function hello() {
+ return 'hi there';
+}
+```
+
+## Normalize whitespace
+
+When the code is represented, it's _order_ is significant, but its physical location is not.
+This means that whitespace is normalized.
+The following examples are equivalent:
+
+### Example with two-space indentation
+
+```javascript
+function hello() {
+ return 'hello world';
+}
+```
+
+### Example with four-space indentation
+
+```javascript
+function hello() {
+ return 'hello world';
+}
+```
+
+### Example with interesting indentation
+
+```javascript
+function hello() {
+ return 'hello world';
+}
+```
+
+## Normalize identifiers
+
+Identifiers are normalized to a placeholder value.
+
+### Before
+
+```javascript
+const MY_CONSTANT = 42;
+
+function answer(multiplier, addition = 1) {
+ return MY_CONSTANT * multiplier + addition;
+}
+```
+
+### After
+
+```javascript
+const PLACEHOLDER_1 = 42;
+
+function PLACEHOLDER_2(PLACEHOLDER_3, PLACEHOLDER_4 = 1) {
+ return PLACEHOLDER_1 * PLACEHOLDER_3 + PLACEHOLDER_4;
+}
+```
+
+[github-javascript-representer]: https://github.com/exercism/javascript-representer
diff --git a/docs/TESTS.md b/docs/TESTS.md
index 5923ad06f9..c45bf03a65 100644
--- a/docs/TESTS.md
+++ b/docs/TESTS.md
@@ -3,20 +3,20 @@
Execute the tests with:
```shell
-npm run test
+corepack pnpm test
```
Be sure your code follows best practices and coding styles, as other users do, with ESLint, a tool to perform static analysis on your code.
Sometimes, tools like this save you some time detecting typos or silly mistakes in your JavaScript code:
```shell
-npm run lint
+corepack pnpm lint
```
You can also run Jest in "watch" mode, which will re-run your tests automatically when you save changes to the code or test module:
```shell
-npm run watch
+corepack pnpm watch
```
## Understanding Skip Tests
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/amusement-park/.eslintrc b/exercises/concept/amusement-park/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/concept/amusement-park/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/concept/amusement-park/.gitignore b/exercises/concept/amusement-park/.gitignore
index bdb912f98a..0c88ff6ec3 100644
--- a/exercises/concept/amusement-park/.gitignore
+++ b/exercises/concept/amusement-park/.gitignore
@@ -1,3 +1,5 @@
-node_modules
-yarn-error.log
-
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/amusement-park/.meta/design.md b/exercises/concept/amusement-park/.meta/design.md
index ed3827a633..d974fff324 100644
--- a/exercises/concept/amusement-park/.meta/design.md
+++ b/exercises/concept/amusement-park/.meta/design.md
@@ -32,26 +32,22 @@ This exercise could benefit from the following rules in the [analyzer][analyzer]
The comment types mentioned below only serve as a proposal.
1. `createVisitor`
-
- `actionable`: If the student used a helper variable, give feedback that the result can be returned directly.
- `celebratory`: If the student used classes, celebrate but let them know it is not necessary throughout this exercise.
- `informative`: If the student did not use the short-hand notation but wrote `name: name` etc instead, let them know how to shorten that.
The solution should be accepted nevertheless.
2. `revokeTicket`
-
- `essential`: Check the ticketId field is not deleted and re-added.
- `celebratory`: If they used a method on a visitor class, celebrate but let them know it is not necessary for this exercise.
3. `ticketStatus`
-
- `essential`: Using a type switch should be discouraged since it is confusing to read because of the `typeof null === 'object'` quirk.
- `informative`: If the student did not use early returns, maybe let them know about this alternative.
- `celebratory`: Congratulate if the student used a template string for the "sold" case
- `celebratory`: Congratulate if the student used a `value` helper variable.
4. `simpleTicketStatus`
-
- `essential`: Check `??` was used and not an if-statement or something else.
- `actionable`: If the student used a helper variable, give feedback that the result can be returned directly.
diff --git a/exercises/concept/amusement-park/amusement-park.js b/exercises/concept/amusement-park/amusement-park.js
index 93f397df61..e7d9cc19da 100644
--- a/exercises/concept/amusement-park/amusement-park.js
+++ b/exercises/concept/amusement-park/amusement-park.js
@@ -10,7 +10,7 @@
* @returns {Visitor} the visitor that was created
*/
export function createVisitor(name, age, ticketId) {
- throw new Error('Please implement the createVisitor function.');
+ throw new Error('Remove this line and implement the function');
}
/**
@@ -20,7 +20,7 @@ export function createVisitor(name, age, ticketId) {
* @returns {Visitor} the visitor without a ticket
*/
export function revokeTicket(visitor) {
- throw new Error('Please implement the revokeTicket function.');
+ throw new Error('Remove this line and implement the function');
}
/**
@@ -31,7 +31,7 @@ export function revokeTicket(visitor) {
* @returns {string} ticket status
*/
export function ticketStatus(tickets, ticketId) {
- throw new Error('Please implement the ticketStatus function.');
+ throw new Error('Remove this line and implement the function');
}
/**
@@ -43,7 +43,7 @@ export function ticketStatus(tickets, ticketId) {
* @returns {string} ticket status
*/
export function simpleTicketStatus(tickets, ticketId) {
- throw new Error('Please implement the simpleTicketStatus function.');
+ throw new Error('Remove this line and implement the function');
}
/**
@@ -53,5 +53,5 @@ export function simpleTicketStatus(tickets, ticketId) {
* @returns {string | undefined} version
*/
export function gtcVersion(visitor) {
- throw new Error('Please implement the gtcVersion function.');
+ throw new Error('Remove this line and implement the function');
}
diff --git a/exercises/concept/amusement-park/amusement-park.spec.js b/exercises/concept/amusement-park/amusement-park.spec.js
index 6b0e7aedae..c25f8d9fc1 100644
--- a/exercises/concept/amusement-park/amusement-park.spec.js
+++ b/exercises/concept/amusement-park/amusement-park.spec.js
@@ -1,9 +1,10 @@
+import { describe, expect, test } from '@jest/globals';
import {
createVisitor,
+ gtcVersion,
revokeTicket,
- ticketStatus,
simpleTicketStatus,
- gtcVersion,
+ ticketStatus,
} from './amusement-park';
describe('createVisitor', () => {
diff --git a/exercises/concept/amusement-park/babel.config.js b/exercises/concept/amusement-park/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/concept/amusement-park/babel.config.js
+++ b/exercises/concept/amusement-park/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/concept/amusement-park/eslint.config.mjs b/exercises/concept/amusement-park/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/amusement-park/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/amusement-park/jest.config.js b/exercises/concept/amusement-park/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/amusement-park/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/amusement-park/package.json b/exercises/concept/amusement-park/package.json
index f2fd8d1602..b517c579f6 100644
--- a/exercises/concept/amusement-park/package.json
+++ b/exercises/concept/amusement-park/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/concept/amusement-park"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/concept/annalyns-infiltration/.eslintrc b/exercises/concept/annalyns-infiltration/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/concept/annalyns-infiltration/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/concept/annalyns-infiltration/.gitignore b/exercises/concept/annalyns-infiltration/.gitignore
index bdb912f98a..0c88ff6ec3 100644
--- a/exercises/concept/annalyns-infiltration/.gitignore
+++ b/exercises/concept/annalyns-infiltration/.gitignore
@@ -1,3 +1,5 @@
-node_modules
-yarn-error.log
-
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/annalyns-infiltration/.meta/exemplar.js b/exercises/concept/annalyns-infiltration/.meta/exemplar.js
index a0cec712e3..1d096c536c 100644
--- a/exercises/concept/annalyns-infiltration/.meta/exemplar.js
+++ b/exercises/concept/annalyns-infiltration/.meta/exemplar.js
@@ -69,7 +69,7 @@ export function canFreePrisoner(
knightIsAwake,
archerIsAwake,
prisonerIsAwake,
- petDogIsPresent
+ petDogIsPresent,
) {
return (
(!knightIsAwake && !archerIsAwake && prisonerIsAwake) ||
diff --git a/exercises/concept/annalyns-infiltration/annalyns-infiltration.js b/exercises/concept/annalyns-infiltration/annalyns-infiltration.js
index e7255e5ea5..0c3e4adfc9 100644
--- a/exercises/concept/annalyns-infiltration/annalyns-infiltration.js
+++ b/exercises/concept/annalyns-infiltration/annalyns-infiltration.js
@@ -69,7 +69,7 @@ export function canFreePrisoner(
knightIsAwake,
archerIsAwake,
prisonerIsAwake,
- petDogIsPresent
+ petDogIsPresent,
) {
throw new Error('Remove this line and implement the function');
}
diff --git a/exercises/concept/annalyns-infiltration/annalyns-infiltration.spec.js b/exercises/concept/annalyns-infiltration/annalyns-infiltration.spec.js
index 7fa874b1d1..d373447db8 100644
--- a/exercises/concept/annalyns-infiltration/annalyns-infiltration.spec.js
+++ b/exercises/concept/annalyns-infiltration/annalyns-infiltration.spec.js
@@ -1,8 +1,9 @@
+import { describe, expect, test } from '@jest/globals';
import {
canExecuteFastAttack,
- canSpy,
- canSignalPrisoner,
canFreePrisoner,
+ canSignalPrisoner,
+ canSpy,
} from './annalyns-infiltration';
describe('can execute fast attack', () => {
@@ -29,7 +30,7 @@ describe('can spy', () => {
const expected = false;
expect(canSpy(knightIsAwake, archerIsAwake, prisonerIsAwake)).toBe(
- expected
+ expected,
);
});
@@ -40,7 +41,7 @@ describe('can spy', () => {
const expected = true;
expect(canSpy(knightIsAwake, archerIsAwake, prisonerIsAwake)).toBe(
- expected
+ expected,
);
});
@@ -51,7 +52,7 @@ describe('can spy', () => {
const expected = true;
expect(canSpy(knightIsAwake, archerIsAwake, prisonerIsAwake)).toBe(
- expected
+ expected,
);
});
@@ -62,7 +63,7 @@ describe('can spy', () => {
const expected = true;
expect(canSpy(knightIsAwake, archerIsAwake, prisonerIsAwake)).toBe(
- expected
+ expected,
);
});
@@ -73,7 +74,7 @@ describe('can spy', () => {
const expected = true;
expect(canSpy(knightIsAwake, archerIsAwake, prisonerIsAwake)).toBe(
- expected
+ expected,
);
});
@@ -84,7 +85,7 @@ describe('can spy', () => {
const expected = true;
expect(canSpy(knightIsAwake, archerIsAwake, prisonerIsAwake)).toBe(
- expected
+ expected,
);
});
@@ -95,7 +96,7 @@ describe('can spy', () => {
const expected = true;
expect(canSpy(knightIsAwake, archerIsAwake, prisonerIsAwake)).toBe(
- expected
+ expected,
);
});
});
@@ -147,8 +148,8 @@ describe('can free prisoner', () => {
knightIsAwake,
archerIsAwake,
prisonerIsAwake,
- petDogIsPresent
- )
+ petDogIsPresent,
+ ),
).toBe(expected);
});
@@ -164,8 +165,8 @@ describe('can free prisoner', () => {
knightIsAwake,
archerIsAwake,
prisonerIsAwake,
- petDogIsPresent
- )
+ petDogIsPresent,
+ ),
).toBe(expected);
});
@@ -181,8 +182,8 @@ describe('can free prisoner', () => {
knightIsAwake,
archerIsAwake,
prisonerIsAwake,
- petDogIsPresent
- )
+ petDogIsPresent,
+ ),
).toBe(expected);
});
@@ -198,8 +199,8 @@ describe('can free prisoner', () => {
knightIsAwake,
archerIsAwake,
prisonerIsAwake,
- petDogIsPresent
- )
+ petDogIsPresent,
+ ),
).toBe(expected);
});
@@ -215,8 +216,8 @@ describe('can free prisoner', () => {
knightIsAwake,
archerIsAwake,
prisonerIsAwake,
- petDogIsPresent
- )
+ petDogIsPresent,
+ ),
).toBe(expected);
});
@@ -232,8 +233,8 @@ describe('can free prisoner', () => {
knightIsAwake,
archerIsAwake,
prisonerIsAwake,
- petDogIsPresent
- )
+ petDogIsPresent,
+ ),
).toBe(expected);
});
@@ -249,8 +250,8 @@ describe('can free prisoner', () => {
knightIsAwake,
archerIsAwake,
prisonerIsAwake,
- petDogIsPresent
- )
+ petDogIsPresent,
+ ),
).toBe(expected);
});
@@ -266,8 +267,8 @@ describe('can free prisoner', () => {
knightIsAwake,
archerIsAwake,
prisonerIsAwake,
- petDogIsPresent
- )
+ petDogIsPresent,
+ ),
).toBe(expected);
});
@@ -283,8 +284,8 @@ describe('can free prisoner', () => {
knightIsAwake,
archerIsAwake,
prisonerIsAwake,
- petDogIsPresent
- )
+ petDogIsPresent,
+ ),
).toBe(expected);
});
@@ -300,8 +301,8 @@ describe('can free prisoner', () => {
knightIsAwake,
archerIsAwake,
prisonerIsAwake,
- petDogIsPresent
- )
+ petDogIsPresent,
+ ),
).toBe(expected);
});
@@ -317,8 +318,8 @@ describe('can free prisoner', () => {
knightIsAwake,
archerIsAwake,
prisonerIsAwake,
- petDogIsPresent
- )
+ petDogIsPresent,
+ ),
).toBe(expected);
});
@@ -334,8 +335,8 @@ describe('can free prisoner', () => {
knightIsAwake,
archerIsAwake,
prisonerIsAwake,
- petDogIsPresent
- )
+ petDogIsPresent,
+ ),
).toBe(expected);
});
@@ -351,8 +352,8 @@ describe('can free prisoner', () => {
knightIsAwake,
archerIsAwake,
prisonerIsAwake,
- petDogIsPresent
- )
+ petDogIsPresent,
+ ),
).toBe(expected);
});
@@ -368,8 +369,8 @@ describe('can free prisoner', () => {
knightIsAwake,
archerIsAwake,
prisonerIsAwake,
- petDogIsPresent
- )
+ petDogIsPresent,
+ ),
).toBe(expected);
});
@@ -385,8 +386,8 @@ describe('can free prisoner', () => {
knightIsAwake,
archerIsAwake,
prisonerIsAwake,
- petDogIsPresent
- )
+ petDogIsPresent,
+ ),
).toBe(expected);
});
@@ -402,8 +403,8 @@ describe('can free prisoner', () => {
knightIsAwake,
archerIsAwake,
prisonerIsAwake,
- petDogIsPresent
- )
+ petDogIsPresent,
+ ),
).toBe(expected);
});
});
diff --git a/exercises/concept/annalyns-infiltration/babel.config.js b/exercises/concept/annalyns-infiltration/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/concept/annalyns-infiltration/babel.config.js
+++ b/exercises/concept/annalyns-infiltration/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/concept/annalyns-infiltration/eslint.config.mjs b/exercises/concept/annalyns-infiltration/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/annalyns-infiltration/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/annalyns-infiltration/jest.config.js b/exercises/concept/annalyns-infiltration/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/annalyns-infiltration/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/annalyns-infiltration/package.json b/exercises/concept/annalyns-infiltration/package.json
index cbbe398cee..48637dbfb5 100644
--- a/exercises/concept/annalyns-infiltration/package.json
+++ b/exercises/concept/annalyns-infiltration/package.json
@@ -13,20 +13,25 @@
"directory": "exercises/concept/annalyns-infiltration"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/concept/appointment-time/.docs/hints.md b/exercises/concept/appointment-time/.docs/hints.md
new file mode 100644
index 0000000000..f089f650fb
--- /dev/null
+++ b/exercises/concept/appointment-time/.docs/hints.md
@@ -0,0 +1,30 @@
+# Hints
+
+## 1. Create an appointment
+
+- You need to create a new date. The introduction elaborates on the different ways.
+- `Date.now()` gives you current time in milliseconds
+- `Date` has several getter methods, listed in the introduction, to get date components. Can you use one of those methods?
+- Likewise, `Date` has matching setter methods to set those components, rolling over into "higher" components if needed.
+
+## 2. Convert a date into a timestamp
+
+- The introduction lists the various ways how to convert a date to different types. Can you use one of those methods?
+
+## 3. Get the details of an appointment
+
+- The introduction has all the required information to extract the information from a date.
+
+## 4. Update an appointment with the given options
+
+- The introduction has all the required information to extract the information from a date.
+- You can reuse `getAppointmentDetails`
+
+## 5. Get available times between two appointments
+
+- General subtraction between two dates will give you the timestamp between the two dates.
+- You probably want to convert to a number first, and not rely on type-coercion.
+
+## 6. Check if an appointment is now valid or not
+
+- Conditional operators will help you to decide which date is bigger or smaller between two dates.
diff --git a/exercises/concept/appointment-time/.docs/instructions.md b/exercises/concept/appointment-time/.docs/instructions.md
new file mode 100644
index 0000000000..cc198e9c35
--- /dev/null
+++ b/exercises/concept/appointment-time/.docs/instructions.md
@@ -0,0 +1,75 @@
+# Instructions
+
+In this exercise you will work on some functions in order to manage appointments.
+The system stores everything in ISO 8601 formatted strings, but that's not how people use the calendar.
+Various functions are necessary to convert between the various formats.
+
+## 1. Create an appointment
+
+Create an appointment `n` days from now at current time.
+The function takes `n` days and return the appointment time of `n` days from now.
+
+```javascript
+createAppointment(4, now);
+// Given now is Sun Oct 05 2022 23:28:43 GMT+0600 (Bangladesh Standard Time)
+// => Sun Oct 09 2022 23:28:43 GMT+0600 (Bangladesh Standard Time)
+```
+
+If the second parameter `now` is unused, the current time should be used instead.
+
+## 2. Convert a date into a timestamp
+
+Various tools only work with the internationally standardized ISO 8601 format.
+Write the function `getAppointmentTimestamp` to take a date and return a string in that format.
+
+```javascript
+const appointment = new Date(Date.UTC(2010, 6, 16, 12, 42, 0, 0));
+
+getAppointmentTimestamp(appointment);
+// => '2010-07-16T12:42:00.000Z'
+```
+
+## 3. Get the details of an appointment
+
+Timestamps are hard to read; a function to get the appointment details should help with that.
+The function `getAppointmentDetails` takes a timestamp in the ISO 8601 format, and returns the year, month, date, hour, and minute.
+
+```javascript
+getAppointmentDetails('2022-04-24T08:15:00.000');
+// => { year: 2022, month: 3, date: 24, hour: 8, minute: 15 }
+```
+
+## 4. Update an appointment with the given options
+
+The function will receive first argument as appointment time and second argument of object of some options.
+You have to update the appointment according to the options in the object and return the new appointment date.
+The options object could have multiple options.
+
+```javascript
+updateAppointment('2022-02-09T09:20:00.000', { month: 6 });
+// => { year: 2022, month: 6, date: 9, hour: 10, minute: 20 }
+```
+
+## 5. Get the available time between two appointments
+
+The function will receive two appointments (timestamps) as arguments.
+You have to return the difference between those two times in seconds.
+
+Because half a second is almost meaningless, round the number before returning it.
+
+```javascript
+timeBetween('2022-12-12T09:20:00.000', '2022-12-18T08:30:00.000');
+// => 515400
+```
+
+## 6. Check if an appointment is now valid or not
+
+Finally, when the appointment is made, the system needs to check if it's valid.
+In other words, the appointment must be in the future, and not the past.
+
+Write the function `isValid` which takes two arguments, an appointment timestamp (string), and the current time as a timestamp (string) and returns `true` if the appointment is in the future, given the current time.
+
+```javascript
+isValid('2022-02-11T23:00:00.000', '2022-02-08T23:00:00.000');
+// => true
+```
diff --git a/exercises/concept/appointment-time/.docs/introduction.md b/exercises/concept/appointment-time/.docs/introduction.md
new file mode 100644
index 0000000000..d4e49404b4
--- /dev/null
+++ b/exercises/concept/appointment-time/.docs/introduction.md
@@ -0,0 +1,192 @@
+# Introduction
+
+JavaScript has a built-in object `Date` which stores date and time, and provides methods for their management.
+
+
+~~~exercism/caution
+It was based on Java's `java.util.Date` class, which was replaced in the early 2010s, but for backwards compatibility, JavaScript's `Date` sticks around.
+
+Because of how hard it is to work with Dates in general and because of how bad or non-existing timezone handling is, many libraries exist such as `moment.js`, `day.js`, `date-fns` and `luxon`.
+None of these are available on Exercism.
+
+In your own projects, do not use a deprecated / unmaintained package such as `moment.js` but rely on more modern alternatives like `luxon`, or the not yet widely available [Temporal][mdn-temporal].
+This exercise focusses on `Date`, which will remain relevant until the end of JavaScript.
+
+[mdn-temporal]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal
+~~~
+
+## Creation
+
+A `Date` object is an instance of the `Date` class.
+It can be created without passing any arguments to the constructor function.
+This results in a `Date` object that represents the current date and time:
+
+```javascript
+const now = new Date();
+// => Thu Apr 14 2022 11:46:08 GMT+0530 (India Standard Time)
+// Shows current day, date and time (in your time zone).
+```
+
+### **Unix timestamp (number)**
+
+If a number is passed in, this will be interpreted as a `timestamp`.
+A timestamp is an integer number representing the number of **milliseconds** that has passed since **1 January 1970 [UTC][defn-utc]+0**.
+
+```javascript
+const epoch = new Date(0);
+// Thu Jan 01 1970 01:00:00 GMT+0100 (Central European Standard Time)
+
+const another = new Date(1749508766627);
+// Tue Jun 10 2025 00:39:26 GMT+0200 (Central European Summer Time)
+```
+
+One may expect `new Date(0)` to generate the "earliest" date object, but JavaScript will convert the date to your local timezone, which means that only those around [GMT / with an UTC+0][defn-gmt] timezone will actually get the [Unix epoch][defn-unix-epoch] value.
+
+### **ISO 8601 timestamp (string)**
+
+You can pass a string value representing a date to the `Date` constructor.
+The **only** format that is consistent across implementations is the [simplified version][mdn-date-string-format] of the internationally recognized and standardized so-called [ISO 8601 timestamp strings][defn-iso8601].
+
+A moment in time at [UTC][defn-gmt] looks like this:
+
+```text
+YYYY-MM-DDTHH:mm:ss.mssZ
+YYYYMMDDTHHmmss.mssZ
+```
+
+Where the following substitutions take place:
+
+| Key | Description | Default |
+| ---- | --------------------------------------------- | ------- |
+| YYYY | The calendar year, represented in 4 digits | |
+| MM | The calendar month, represented in 2 digits | 01 |
+| DD | The calendar day, represented in 2 digits | 01 |
+| T | A literal letter T, separating date from time | |
+| HH | The hours in a 24-hour clock, 2 digits | 00 |
+| mm | The minutes, 2 digits | 00 |
+| ss | The seconds, 2 digits | 00 |
+| mss | The milliseconds, 3 digits | 000 |
+| Z | A literal letter Z, or an offset `+/-HH:mm` | |
+
+The literal letter `Z` indicates UTC (no timezone, no Day Light Savings).
+
+Because there are default values for most components, leaving parts off at the end is valid:
+
+```text
+YYYY-MM-DD
+```
+
+Defaults to a time of 00:00:00.000
+
+If the timestamp does not end in `Z`, and it does not end with `+HH:mm` or `-HH:mm` (indicating a timezone offset), because of historical reasons, the following applies:
+
+> When the time zone offset is absent, date-only forms are interpreted as a UTC time and date-time forms are interpreted as a local time.
+> The interpretation as a UTC time is due to a historical spec error that was not consistent with ISO 8601 but could not be changed due to web compatibility.
+> See [Broken Parser – A Web Reality Issue][ref-broken-parser].
+
+
+~~~exercism/caution
+Other formats that are accepted by `Date.parse` may or may not work.
+When working with Dates in JavaScript, _always_ use an ISO 8601 timestamp when converting from a `string` to a `Date`.
+
+Date-only forms are allowed, but not all ISO 8601 formats are supported.
+Consult the [simplified version explanation page on MDN][mdn-date-string-format].
+
+[mdn-date-string-format]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format
+~~~
+
+### **Date object**
+
+An existing date object can also be used as a constructor argument.
+This makes a copy of the existing `Date` object with the same date and time.
+
+```javascript
+const t1 = new Date();
+const t2 = new Date(t1);
+// Values of t1 and t2 will be the same.
+```
+
+### **Supplying individual date and time component values**
+
+A date representing a date can be created by passing three numbers.
+A date representing a date and time can be created by passing in 6 numbers.
+
+```javascript
+const date1 = new Date(95, 11, 17);
+// Creates Date for Dec 17 1995 00:00 if your local timezone is equivalent to UTC.
+
+const date2 = new Date(2013, 12, 5, 13, 24, 0);
+// Creates Date for Jan 5 2014 13:24 if your local timezone is equivalent to UTC.
+```
+
+The second value is the `month`, which starts at `0` for January, up to `11` for December.
+
+## `Date.parse()`
+
+You may find mentions of or references to a date parsing function `Date.parse`.
+Because there are only a few invariants (truths) for this function and because the rest of the implementation is not specified (and thus not standard), one should not use it.
+
+## Accessing `Date` components
+
+There are various methods on date objects that return the components of the date:
+
+```javascript
+getFullYear(); // Get the year (4 digits)
+getMonth(); // Get the month, from 0 to 11.
+getDate(); // Get the day of month, from 1 to 31.
+getHours(); // Get the hour in a 24 clock, from 0 to 23
+getMinutes(); // Get the minutes, from 0 to 59
+getSeconds(); // Get the seconds, from 0 to 59
+getMilliseconds(); // Get the milliseconds, from 0 and 999
+getDay(); // Get the day of week, from 0 (Sunday) to 6 (Saturday).
+```
+
+Each of these has an applicable `set` variant (e.g. `setFullYear`) to update the value.
+Any overflowing value rolls over to the next component:
+
+```javascript
+const date = new Date('2025-02-28T12:42:00Z');
+// => Fri Feb 28 2025 13:42:00 GMT+0100 (Central European Standard Time)
+
+date.setDate(29);
+// there was no February 29th in 2025.
+
+date.getDate();
+// => 1
+
+date.toString();
+// => Sat Mar 01 2025 13:42:00 GMT+0100 (Central European Standard Time)
+```
+
+There are UTC variants for all the methods that disregard the local timezone.
+
+## Converting from date
+
+Date objects have a method `getTime()` that returns the UNIX timestamp in milliseconds, ie. amount of milliseconds the midnight at the beginning of January 1, 1970, UTC.
+Additionally, a method `toISOString()` is available to convert from a date object to a ISO 8601 timestamp string.
+
+## Comparing Dates
+
+Greater than (`>`) and greater than or equals (`>=`) as well as less than (`<`) and less than or equals (`<=`) can be used directly between two dates or a date and a number.
+This works because JavaScript will try to coerce the date to a primitive.
+
+
+~~~exercism/advanced
+When doing a comparison between two dates or date and a number, JavaScript calls [`[Symbol.toPrimitive]("number")`][mdn-to-primitive] which internally calls [`date.valueOf()`][mdn-date-value-of].
+The latter is the same as calling [`date.getTime()`][mdn-date-get-time].
+
+If you do not want to rely on this behaviour, convert to a number using `getTime()` first.
+
+[mdn-to-primitive]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Symbol.toPrimitive
+[mdn-date-value-of]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/valueOf
+[mdn-date-get-time]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getTime
+~~~
+
+Dates cannot be compared using equality (`==`, and `===`), but the result of `.getTime()` can.
+
+[defn-utc]: https://simple.wikipedia.org/wiki/Coordinated_Universal_Time
+[defn-gmt]: https://simple.wikipedia.org/wiki/Greenwich_Mean_Time
+[defn-unix-epoch]: https://en.wikipedia.org/wiki/Epoch_%28computing%29
+[defn-iso8601]: https://en.wikipedia.org/wiki/ISO_8601
+[mdn-date-string-format]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format
+[ref-broken-parser]: https://maggiepint.com/2017/04/11/fixing-javascript-date-web-compatibility-and-reality/
diff --git a/exercises/concept/appointment-time/.gitignore b/exercises/concept/appointment-time/.gitignore
new file mode 100644
index 0000000000..0c88ff6ec3
--- /dev/null
+++ b/exercises/concept/appointment-time/.gitignore
@@ -0,0 +1,5 @@
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/appointment-time/.meta/config.json b/exercises/concept/appointment-time/.meta/config.json
new file mode 100644
index 0000000000..649f219e32
--- /dev/null
+++ b/exercises/concept/appointment-time/.meta/config.json
@@ -0,0 +1,27 @@
+{
+ "authors": [
+ "SalahuddinAhammed",
+ "SleeplessByte"
+ ],
+ "contributors": [
+ "BadIdeaException"
+ ],
+ "files": {
+ "solution": [
+ "appointment-time.js"
+ ],
+ "test": [
+ "appointment-time.spec.js"
+ ],
+ "exemplar": [
+ ".meta/exemplar.js"
+ ]
+ },
+ "blurb": "Learn how to work with dates by managing appointments",
+ "custom": {
+ "version.tests.compatibility": "jest-29",
+ "flag.tests.task-per-describe": true,
+ "flag.tests.may-run-long": false,
+ "flag.tests.includes-optional": false
+ }
+}
diff --git a/exercises/concept/appointment-time/.meta/exemplar.js b/exercises/concept/appointment-time/.meta/exemplar.js
new file mode 100644
index 0000000000..ff47aad2fc
--- /dev/null
+++ b/exercises/concept/appointment-time/.meta/exemplar.js
@@ -0,0 +1,90 @@
+// @ts-check
+
+/**
+ * Create an appointment
+ *
+ * @param {number} days
+ * @param {number} [now] (ms since the epoch, or undefined)
+ *
+ * @returns {Date} the appointment
+ */
+export function createAppointment(days, now = Date.now()) {
+ const date = new Date(now);
+ date.setDate(date.getDate() + days);
+
+ return date;
+}
+
+/**
+ * Generate the appointment timestamp
+ *
+ * @param {Date} appointmentDate
+ *
+ * @returns {string} timestamp
+ */
+export function getAppointmentTimestamp(appointmentDate) {
+ return appointmentDate.toISOString();
+}
+
+/**
+ * Get details of an appointment
+ *
+ * @param {string} timestamp (ISO 8601)
+ *
+ * @returns {Record<'year' | 'month' | 'date' | 'hour' | 'minute', number>} the appointment details
+ */
+export function getAppointmentDetails(timestamp) {
+ const appointmentDate = new Date(timestamp);
+
+ return {
+ year: appointmentDate.getFullYear(),
+ month: appointmentDate.getMonth(),
+ date: appointmentDate.getDate(),
+ hour: appointmentDate.getHours(),
+ minute: appointmentDate.getMinutes(),
+ };
+}
+
+/**
+ * Update an appointment with given options
+ *
+ * @param {string} timestamp (ISO8601)
+ * @param {Partial>} options
+ *
+ * @returns {Record<'year' | 'month' | 'date' | 'hour' | 'minute', number>} the appointment details
+ */
+export function updateAppointment(timestamp, options) {
+ let appointmentDate = new Date(timestamp);
+
+ if (options.year !== undefined) appointmentDate.setFullYear(options.year);
+ if (options.month !== undefined) appointmentDate.setMonth(options.month);
+ if (options.date !== undefined) appointmentDate.setDate(options.date);
+ if (options.hour !== undefined) appointmentDate.setHours(options.hour);
+ if (options.minute !== undefined) appointmentDate.setMinutes(options.minute);
+
+ return getAppointmentDetails(appointmentDate.toISOString());
+}
+
+/**
+ * Get available time in seconds (rounded) between two appointments
+ *
+ * @param {string} timestampA (ISO 8601)
+ * @param {string} timestampB (ISO 8601)
+ *
+ * @returns {number} amount of seconds (rounded)
+ */
+export function timeBetween(timestampA, timestampB) {
+ return Math.round(
+ (new Date(timestampB).getTime() - new Date(timestampA).getTime()) / 1000,
+ );
+}
+
+/**
+ * Checks if the appointment is in the past
+ *
+ * @param {string} appointmentTimestamp (ISO 8601)
+ * @param {string} currentTimestamp (ISO 8601)
+ */
+export function isValid(appointmentTimestamp, currentTimestamp) {
+ return new Date(appointmentTimestamp) > new Date(currentTimestamp);
+}
diff --git a/exercises/concept/appointment-time/.npmrc b/exercises/concept/appointment-time/.npmrc
new file mode 100644
index 0000000000..d26df800bb
--- /dev/null
+++ b/exercises/concept/appointment-time/.npmrc
@@ -0,0 +1 @@
+audit=false
diff --git a/exercises/concept/appointment-time/LICENSE b/exercises/concept/appointment-time/LICENSE
new file mode 100644
index 0000000000..90e73be03b
--- /dev/null
+++ b/exercises/concept/appointment-time/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Exercism
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/exercises/concept/appointment-time/appointment-time.js b/exercises/concept/appointment-time/appointment-time.js
new file mode 100644
index 0000000000..01af573b48
--- /dev/null
+++ b/exercises/concept/appointment-time/appointment-time.js
@@ -0,0 +1,69 @@
+// @ts-check
+
+/**
+ * Create an appointment
+ *
+ * @param {number} days
+ * @param {number} [now] (ms since the epoch, or undefined)
+ *
+ * @returns {Date} the appointment
+ */
+export function createAppointment(days, now = undefined) {
+ throw new Error('Remove this line and implement the function');
+}
+
+/**
+ * Generate the appointment timestamp
+ *
+ * @param {Date} appointmentDate
+ *
+ * @returns {string} timestamp
+ */
+export function getAppointmentTimestamp(appointmentDate) {
+ throw new Error('Remove this line and implement the function');
+}
+
+/**
+ * Get details of an appointment
+ *
+ * @param {string} timestamp (ISO 8601)
+ *
+ * @returns {Record<'year' | 'month' | 'date' | 'hour' | 'minute', number>} the appointment details
+ */
+export function getAppointmentDetails(timestamp) {
+ throw new Error('Remove this line and implement the function');
+}
+
+/**
+ * Update an appointment with given options
+ *
+ * @param {string} timestamp (ISO 8601)
+ * @param {Partial>} options
+ *
+ * @returns {Record<'year' | 'month' | 'date' | 'hour' | 'minute', number>} the appointment details
+ */
+export function updateAppointment(timestamp, options) {
+ throw new Error('Remove this line and implement the function');
+}
+
+/**
+ * Get available time in seconds (rounded) between two appointments
+ *
+ * @param {string} timestampA (ISO 8601)
+ * @param {string} timestampB (ISO 8601)
+ *
+ * @returns {number} amount of seconds (rounded)
+ */
+export function timeBetween(timestampA, timestampB) {
+ throw new Error('Remove this line and implement the function');
+}
+
+/**
+ * Get available times between two appointment
+ *
+ * @param {string} appointmentTimestamp (ISO 8601)
+ * @param {string} currentTimestamp (ISO 8601)
+ */
+export function isValid(appointmentTimestamp, currentTimestamp) {
+ throw new Error('Remove this line and implement the function');
+}
diff --git a/exercises/concept/appointment-time/appointment-time.spec.js b/exercises/concept/appointment-time/appointment-time.spec.js
new file mode 100644
index 0000000000..d0872cc2b3
--- /dev/null
+++ b/exercises/concept/appointment-time/appointment-time.spec.js
@@ -0,0 +1,151 @@
+import { describe, test, expect } from '@jest/globals';
+
+import {
+ createAppointment,
+ getAppointmentTimestamp,
+ getAppointmentDetails,
+ isValid,
+ updateAppointment,
+ timeBetween,
+} from './appointment-time';
+
+describe('createAppointment', () => {
+ test('uses the passed in current time', () => {
+ const currentTime = Date.UTC(2000, 6, 16, 12, 0, 0, 0);
+ const result = createAppointment(0, currentTime);
+
+ expect(result).toEqual(new Date(currentTime));
+ });
+
+ test('uses the actual current time when it is not passed in', () => {
+ const currentTime = Date.now();
+ const result = createAppointment(0);
+
+ expect(result).toEqual(new Date(currentTime));
+ });
+
+ test('creates appointment without DST change', () => {
+ const offset = 4; // days
+
+ const currentTime = Date.UTC(2000, 6, 1, 12, 0, 0, 0);
+ const expectedTime = currentTime + offset * 24 * 60 * 60 * 1000;
+
+ expect(createAppointment(offset, currentTime)).toEqual(
+ new Date(expectedTime),
+ );
+ });
+
+ test('creates appointment with potential DST change', () => {
+ const offset = 180; // days
+
+ const currentTime = Date.UTC(2000, 6, 1, 12, 0, 0, 0);
+ let expectedTime = currentTime + offset * 24 * 60 * 60 * 1000;
+ // Manually adjust for DST timezone offset:
+ expectedTime +=
+ (new Date(expectedTime).getTimezoneOffset() -
+ new Date(currentTime).getTimezoneOffset()) *
+ 60 *
+ 1000;
+
+ expect(createAppointment(offset, currentTime)).toEqual(
+ new Date(expectedTime),
+ );
+ });
+
+ test('rolls over days, months, and years', () => {
+ const currentTime = Date.UTC(1991, 16, 6, 2, 12, 0, 0);
+ const result = createAppointment(720, currentTime);
+
+ expect(result.getTime()).toStrictEqual(767326320000);
+ });
+});
+
+describe('getAppointmentTimestamp', () => {
+ test('returns the correct timestamp', () => {
+ const currentTime = new Date(Date.UTC(2000, 6, 16, 12, 0, 0, 0));
+
+ expect(getAppointmentTimestamp(currentTime)).toEqual(
+ '2000-07-16T12:00:00.000Z',
+ );
+ });
+});
+
+describe('getAppointment', () => {
+ test('extracts appointment details', () => {
+ expect(getAppointmentDetails('2022-04-24T08:15:00.000')).toStrictEqual({
+ year: 2022,
+ month: 3,
+ date: 24,
+ hour: 8,
+ minute: 15,
+ });
+ });
+});
+
+describe('updateAppointment', () => {
+ test('updates a field', () => {
+ expect(
+ updateAppointment('2022-02-09T09:20:00.000', { month: 6 }),
+ ).toStrictEqual({ year: 2022, month: 6, date: 9, hour: 9, minute: 20 });
+ });
+
+ test('update multiple fields', () => {
+ expect(
+ updateAppointment('2022-11-21T21:20:00.000', {
+ year: 2023,
+ month: 1,
+ date: 12,
+ hour: 1,
+ minute: 29,
+ }),
+ ).toStrictEqual({ year: 2023, month: 1, date: 12, hour: 1, minute: 29 });
+ });
+
+ test('updates even if option is 0', () => {
+ expect(
+ updateAppointment('2022-12-17T07:10:00.000', { minute: 0 }),
+ ).toStrictEqual({ year: 2022, month: 11, date: 17, hour: 7, minute: 0 });
+ });
+
+ test('rolls over values', () => {
+ expect(
+ updateAppointment('2029-02-28T23:59:00.000', { hour: 24, minute: 60 }),
+ ).toStrictEqual({ year: 2029, month: 2, date: 1, hour: 1, minute: 0 });
+ });
+});
+
+describe('availableTimes', () => {
+ test('retrieves number of seconds between two appointments', () => {
+ expect(
+ timeBetween('2022-12-12T09:20:00.000', '2022-12-18T08:30:00.000'),
+ ).toBe(515400);
+ });
+
+ test('rounds to seconds', () => {
+ expect(
+ timeBetween('2024-03-06T09:12:15.180', '2024-03-06T18:15:12.090'),
+ ).toBe(32577);
+ });
+});
+
+describe('isValid', () => {
+ test('is true when appointment datetime is in the future', () => {
+ expect(isValid('2022-02-11T23:00:00.000', '2022-02-08T23:00:00.000')).toBe(
+ true,
+ );
+ });
+
+ test('is true when appointment date is in the future', () => {
+ expect(isValid('2022-02-11', '2022-02-08')).toBe(true);
+ });
+
+ test('is false when appointment datetime is in the past', () => {
+ expect(isValid('2022-05-20T23:00:00.000', '2023-02-08T23:00:00.000')).toBe(
+ false,
+ );
+ });
+
+ test('is false when appointment date is in the past', () => {
+ expect(isValid('2022-05-21', '2022-05-22')).toBe(false);
+ });
+});
diff --git a/exercises/concept/appointment-time/babel.config.js b/exercises/concept/appointment-time/babel.config.js
new file mode 100644
index 0000000000..a638497df1
--- /dev/null
+++ b/exercises/concept/appointment-time/babel.config.js
@@ -0,0 +1,4 @@
+module.exports = {
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
+ plugins: [],
+};
diff --git a/exercises/concept/appointment-time/eslint.config.mjs b/exercises/concept/appointment-time/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/appointment-time/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/appointment-time/jest.config.js b/exercises/concept/appointment-time/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/appointment-time/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/appointment-time/package.json b/exercises/concept/appointment-time/package.json
new file mode 100644
index 0000000000..65e47ebe13
--- /dev/null
+++ b/exercises/concept/appointment-time/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "@exercism/javascript-appointment-time",
+ "description": "Exercism concept exercise on appointment-time",
+ "author": "Katrina Owen",
+ "contributors": [
+ "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/concept/appointment-time"
+ },
+ "devDependencies": {
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
+ },
+ "dependencies": {},
+ "scripts": {
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
+}
diff --git a/exercises/concept/bird-watcher/.docs/instructions.md b/exercises/concept/bird-watcher/.docs/instructions.md
index 7ff05a32f6..a0dda80ee9 100644
--- a/exercises/concept/bird-watcher/.docs/instructions.md
+++ b/exercises/concept/bird-watcher/.docs/instructions.md
@@ -6,15 +6,18 @@ You already digitalized the bird counts per day for the past weeks that you kept
Now you want to determine the total number of birds that you counted, calculate the bird count for a specific week and correct a counting mistake.
-```exercism/note
-To practice, use a for loop to solve each of the tasks below.
-```
+
+~~~~exercism/note
+To practice, use a `for` loop to solve each of the tasks below.
+~~~~
## 1. Determine the total number of birds that you counted so far
-Let us start analyzing the data by getting a high-level view. Find out how many birds you counted in total since you started your logs.
+Let us start analyzing the data by getting a high-level view.
+Find out how many birds you counted in total since you started your logs.
-Implement a function `totalBirdCount` that accepts an array that contains the bird count per day. It should return the total number of birds that you counted.
+Implement a function `totalBirdCount` that accepts an array-like object that contains the bird count per day.
+It should return the total number of birds that you counted.
```javascript
birdsPerDay = [2, 5, 0, 7, 4, 1, 3, 0, 2, 5, 0, 1, 3, 1];
@@ -26,8 +29,9 @@ totalBirdCount(birdsPerDay);
Now that you got a general feel for your bird count numbers, you want to make a more fine-grained analysis.
-Implement a function `birdsInWeek` that accepts an array of bird counts per day and a week number.
-It returns the total number of birds that you counted in that specific week. You can assume weeks are always tracked completely.
+Implement a function `birdsInWeek` that accepts an array-like object of bird counts per day and a week number.
+It returns the total number of birds that you counted in that specific week.
+You can assume weeks are always tracked completely.
```javascript
birdsPerDay = [2, 5, 0, 7, 4, 1, 3, 0, 2, 5, 0, 1, 3, 1];
@@ -42,7 +46,7 @@ You figured out that this bird always spent every second day in your garden.
You do not know exactly where it was in between those days but definitely not in your garden.
Your bird watcher intuition also tells you that the bird was in your garden on the first day that you tracked in your list.
-Given this new information, write a function `fixBirdCountLog` that takes an array of birds counted per day as an argument. It should correct the counting mistake and return the modified array.
+Given this new information, write a function `fixBirdCountLog` that takes an array-like object of birds counted per day as an argument. It should correct the counting mistake by modifying the given array.
```javascript
birdsPerDay = [2, 5, 0, 7, 4, 1];
diff --git a/exercises/concept/bird-watcher/.eslintrc b/exercises/concept/bird-watcher/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/concept/bird-watcher/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/concept/bird-watcher/.gitignore b/exercises/concept/bird-watcher/.gitignore
index bdb912f98a..0c88ff6ec3 100644
--- a/exercises/concept/bird-watcher/.gitignore
+++ b/exercises/concept/bird-watcher/.gitignore
@@ -1,3 +1,5 @@
-node_modules
-yarn-error.log
-
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/bird-watcher/.meta/design.md b/exercises/concept/bird-watcher/.meta/design.md
index 554e26f2c7..1b297b64d0 100644
--- a/exercises/concept/bird-watcher/.meta/design.md
+++ b/exercises/concept/bird-watcher/.meta/design.md
@@ -36,19 +36,16 @@ This exercise could benefit from the following rules in the [analyzer][analyzer]
For all tasks check that the student actually used a for loop.
1. `totalBirdCount`
-
- Verify that the condition is written with `< x.length` instead of `<= y.length -1`.
- Check whether a shorthand assignment `+=` was used to increase the sum (non-essential feedback).
- Verify the total was properly initialized with `0` instead of e.g. `null`
- Verify the increment operator was used in loop header step
2. `birdsInWeek`
-
- Verify a helper variable was used instead of duplicating the calculation in the initialization and condition of the loop
- Other checks should be the same as for `totalBirdCount`
3. `fixBirdCountLog`
-
- Check whether a shorthand assignment `+=` was used to increase the loop counter (non-essential feedback)
- Check whether the increment operator was used in the loop body
diff --git a/exercises/concept/bird-watcher/.meta/exemplar.js b/exercises/concept/bird-watcher/.meta/exemplar.js
index 616b6c8877..8eb350b240 100644
--- a/exercises/concept/bird-watcher/.meta/exemplar.js
+++ b/exercises/concept/bird-watcher/.meta/exemplar.js
@@ -12,9 +12,11 @@
*/
export function totalBirdCount(birdsPerDay) {
let total = 0;
+
for (let i = 0; i < birdsPerDay.length; i++) {
total += birdsPerDay[i];
}
+
return total;
}
@@ -27,10 +29,12 @@ export function totalBirdCount(birdsPerDay) {
*/
export function birdsInWeek(birdsPerDay, week) {
let total = 0;
+
const start = 7 * (week - 1);
for (let i = start; i < start + 7; i++) {
total += birdsPerDay[i];
}
+
return total;
}
@@ -39,11 +43,10 @@ export function birdsInWeek(birdsPerDay, week) {
* by one for every second day.
*
* @param {number[]} birdsPerDay
- * @returns {number[]} corrected bird count data
+ * @returns {void} should not return anything
*/
export function fixBirdCountLog(birdsPerDay) {
for (let i = 0; i < birdsPerDay.length; i += 2) {
birdsPerDay[i]++;
}
- return birdsPerDay;
}
diff --git a/exercises/concept/bird-watcher/babel.config.js b/exercises/concept/bird-watcher/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/concept/bird-watcher/babel.config.js
+++ b/exercises/concept/bird-watcher/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/concept/bird-watcher/bird-watcher.js b/exercises/concept/bird-watcher/bird-watcher.js
index c91a863c31..d94a707931 100644
--- a/exercises/concept/bird-watcher/bird-watcher.js
+++ b/exercises/concept/bird-watcher/bird-watcher.js
@@ -11,7 +11,7 @@
* @returns {number} total bird count
*/
export function totalBirdCount(birdsPerDay) {
- throw new Error('Please implement the totalBirdCount function');
+ throw new Error('Remove this line and implement the function');
}
/**
@@ -22,7 +22,7 @@ export function totalBirdCount(birdsPerDay) {
* @returns {number} birds counted in the given week
*/
export function birdsInWeek(birdsPerDay, week) {
- throw new Error('Please implement the birdsInWeek function');
+ throw new Error('Remove this line and implement the function');
}
/**
@@ -30,8 +30,8 @@ export function birdsInWeek(birdsPerDay, week) {
* by one for every second day.
*
* @param {number[]} birdsPerDay
- * @returns {number[]} corrected bird count data
+ * @returns {void} should not return anything
*/
export function fixBirdCountLog(birdsPerDay) {
- throw new Error('Please implement the fixBirdCountLog function');
+ throw new Error('Remove this line and implement the function');
}
diff --git a/exercises/concept/bird-watcher/bird-watcher.spec.js b/exercises/concept/bird-watcher/bird-watcher.spec.js
index dbd630a816..bbedf2210e 100644
--- a/exercises/concept/bird-watcher/bird-watcher.spec.js
+++ b/exercises/concept/bird-watcher/bird-watcher.spec.js
@@ -1,45 +1,97 @@
-import { totalBirdCount, birdsInWeek, fixBirdCountLog } from './bird-watcher';
+import { describe, expect, test } from '@jest/globals';
+import { birdsInWeek, fixBirdCountLog, totalBirdCount } from './bird-watcher';
+
+const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom');
+const customLogSymbol = Symbol.for('exercism.javascript.util.log');
+
+// Follow the instructions in case you are stuck on "list.method is not a function"
+class CountingReport {
+ constructor(counts) {
+ // Enables array[index]
+ counts.forEach((count, index) => {
+ this[index] = count;
+ });
+
+ // Enables .length
+ this.length = counts.length;
+ }
+
+ // Log value in non-upgraded environments
+ toString() {
+ return arrayOf(this).toString();
+ }
+
+ // Overrides logging in node (ie. students working locally)
+ [customInspectSymbol]() {
+ return `Seen birds per day: ${arrayOf(this)}`;
+ }
+
+ // Overrides log overrides in web environment (ie. students working in editor)
+ [customLogSymbol]() {
+ return `Seen birds per day: ${arrayOf(this)}`;
+ }
+}
+
+function report(...values) {
+ return new CountingReport(values);
+}
+
+function arrayOf(countingReport) {
+ return Array.from(
+ { length: countingReport.length },
+ (_, i) => countingReport[i],
+ );
+}
+
+function randomArray(length) {
+ return Array.from({ length }, () => Math.floor(Math.random() * 8));
+}
describe('totalBirdCount', () => {
test('calculates the correct total number of birds', () => {
- const birdsPerDay = [9, 0, 8, 4, 5, 1, 3];
+ const birdsPerDay = report(9, 0, 8, 4, 5, 1, 3);
expect(totalBirdCount(birdsPerDay)).toBe(30);
});
test('works for a short bird count list', () => {
- const birdsPerDay = [2];
+ const birdsPerDay = report(2);
expect(totalBirdCount(birdsPerDay)).toBe(2);
});
test('works for a long bird count list', () => {
// prettier-ignore
- const birdsPerDay = [2, 8, 4, 1, 3, 5, 0, 4, 1, 6, 0, 3, 0, 1, 5, 4, 1, 1, 2, 6];
+ const birdsPerDay = report(
+ 2, 8, 4, 1, 3, 5, 0, 4, 1, 6, 0, 3, 0, 1, 5, 4, 1, 1, 2, 6
+ );
+
expect(totalBirdCount(birdsPerDay)).toBe(57);
});
});
describe('birdsInWeek', () => {
test('calculates the number of birds in the first week', () => {
- const birdsPerDay = [3, 0, 5, 1, 0, 4, 1, 0, 3, 4, 3, 0, 8, 0];
+ const birdsPerDay = report(3, 0, 5, 1, 0, 4, 1, 0, 3, 4, 3, 0, 8, 0);
expect(birdsInWeek(birdsPerDay, 1)).toBe(14);
});
test('calculates the number of birds for a week in the middle of the log', () => {
// prettier-ignore
- const birdsPerDay = [4, 7, 3, 2, 1, 1, 2, 0, 2, 3, 2, 7, 1, 3, 0, 6, 5, 3, 7, 2, 3];
+ const birdsPerDay = report(4, 7, 3, 2, 1, 1, 2, 0, 2, 3, 2, 7, 1, 3, 0, 6, 5, 3, 7, 2, 3);
expect(birdsInWeek(birdsPerDay, 2)).toBe(18);
});
test('works when there is only one week', () => {
- const birdsPerDay = [3, 0, 3, 3, 2, 1, 0];
+ const birdsPerDay = report(3, 0, 3, 3, 2, 1, 0);
expect(birdsInWeek(birdsPerDay, 1)).toBe(12);
});
test('works for a long bird count list', () => {
- const week21 = [2, 0, 1, 4, 1, 3, 0];
- const birdsPerDay = randomArray(20 * 7)
- .concat(week21)
- .concat(randomArray(10 * 7));
+ const week21 = report(2, 0, 1, 4, 1, 3, 0);
+ const birdsPerDay = report(
+ ...randomArray(20 * 7)
+ .concat(arrayOf(week21))
+ .concat(randomArray(10 * 7)),
+ );
expect(birdsInWeek(birdsPerDay, 21)).toBe(11);
});
@@ -47,32 +99,32 @@ describe('birdsInWeek', () => {
describe('fixBirdCountLog', () => {
test('returns a bird count list with the corrected values', () => {
- const birdsPerDay = [3, 0, 5, 1, 0, 4, 1, 0, 3, 4, 3, 0];
+ const birdsPerDay = report(3, 0, 5, 1, 0, 4, 1, 0, 3, 4, 3, 0);
const expected = [4, 0, 6, 1, 1, 4, 2, 0, 4, 4, 4, 0];
- expect(fixBirdCountLog(birdsPerDay)).toEqual(expected);
- });
-
- test('does not create a new array', () => {
- const birdsPerDay = [2, 0, 1, 4, 1, 3, 0];
+ fixBirdCountLog(birdsPerDay);
- // This checks that the same object that was passed in is returned.
- // https://jestjs.io/docs/expect#tobevalue
- expect(Object.is(fixBirdCountLog(birdsPerDay), birdsPerDay)).toBe(true);
+ expect(arrayOf(birdsPerDay)).toEqual(expected);
});
test('works for a short bird count list', () => {
- expect(fixBirdCountLog([4, 2])).toEqual([5, 2]);
+ const birdsPerDay = report(4, 2);
+ fixBirdCountLog(birdsPerDay);
+
+ expect(arrayOf(birdsPerDay)).toEqual([5, 2]);
});
test('works for a long bird count list', () => {
// prettier-ignore
- const birdsPerDay = [2, 8, 4, 1, 3, 5, 0, 4, 1, 6, 0, 3, 0, 1, 5, 4, 1, 1, 2, 6];
+ const birdsPerDay = report(
+ 2, 8, 4, 1, 3, 5, 0, 4, 1, 6, 0, 3, 0, 1, 5, 4, 1, 1, 2, 6
+ );
+
// prettier-ignore
- const expected = [3, 8, 5, 1, 4, 5, 1, 4, 2, 6, 1, 3, 1, 1, 6, 4, 2, 1, 3, 6];
- expect(fixBirdCountLog(birdsPerDay)).toEqual(expected);
+ const expected = [
+ 3, 8, 5, 1, 4, 5, 1, 4, 2, 6, 1, 3, 1, 1, 6, 4, 2, 1, 3, 6
+ ]
+
+ fixBirdCountLog(birdsPerDay);
+ expect(arrayOf(birdsPerDay)).toEqual(expected);
});
});
-
-function randomArray(length) {
- return Array.from({ length: length }, () => Math.floor(Math.random() * 8));
-}
diff --git a/exercises/concept/bird-watcher/eslint.config.mjs b/exercises/concept/bird-watcher/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/bird-watcher/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/bird-watcher/jest.config.js b/exercises/concept/bird-watcher/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/bird-watcher/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/bird-watcher/package.json b/exercises/concept/bird-watcher/package.json
index 8e1d90f4d9..300e231da1 100644
--- a/exercises/concept/bird-watcher/package.json
+++ b/exercises/concept/bird-watcher/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/concept/bird-watcher"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/concept/captains-log/.docs/hints.md b/exercises/concept/captains-log/.docs/hints.md
new file mode 100644
index 0000000000..ee4730cc40
--- /dev/null
+++ b/exercises/concept/captains-log/.docs/hints.md
@@ -0,0 +1,16 @@
+# Hints
+
+## 1. Generate a random starship registry number
+
+- To generate a random number in the range _min_ (inclusive) to _max_ (exclusive) you can use the snippet `min + Math.random()*(max - min)`.
+- There is a [built in function][floor] for turning a floating point number into an integer.
+
+## 2.Generate a random stardate
+
+- To generate a random number in the range _min_ (inclusive) to _max_ (exclusive) you can use the snippet `min + Math.random()*(max - min)`.
+
+## 3. Generate a random planet
+
+- You can use a randomly generated integer as an array index.
+
+[floor]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/floor
diff --git a/exercises/concept/captains-log/.docs/instructions.md b/exercises/concept/captains-log/.docs/instructions.md
new file mode 100644
index 0000000000..cb70343718
--- /dev/null
+++ b/exercises/concept/captains-log/.docs/instructions.md
@@ -0,0 +1,52 @@
+# Instructions
+
+Mary is a big fan of the TV series Star Trek: The Next Generation.
+She often plays pen-and-paper role playing games, where she and her friends pretend to be the crew of the Starship Enterprise.
+Mary's character is Captain Picard, which means she has to keep the captain's log.
+She loves the creative part of the game, but doesn't like to generate random data on the spot.
+
+Help Mary by creating random generators for data commonly appearing in the captain's log.
+
+## 1. Generate a random starship registry number
+
+Enterprise (registry number NCC-1701) is not the only starship flying around!
+When it meets another starship, Mary needs to log the registry number of that starship.
+
+Registry numbers start with the prefix "NCC-" and then use a number from 1000 to 9999 (both inclusive).
+
+Implement the `randomShipRegistryNumber()` function that returns a random starship registry number.
+
+```javascript
+randomShipRegistryNumber();
+// => "NCC-1947"
+```
+
+## 2. Generate a random stardate
+
+What's the use of a log if it doesn't include dates?
+
+A stardate is a floating point number.
+The adventures of the Starship Enterprise from the first season of The Next Generation take place between the stardates 41000.0 and 42000.0.
+The "4" stands for the 24th century, the "1" for the first season.
+
+Implement the function `randomStardate` that returns a floating point number between 41000.0 (inclusive) and 42000.0 (exclusive).
+
+```javascript
+randomStardate();
+// => 41458.15721310934
+```
+
+## 3. Generate a random planet
+
+The Starship Enterprise encounters many planets in its travels.
+Planets in the Star Trek universe are split into categories based on their properties.
+For example, Earth is a class M planet.
+All possible planetary classes are: D, H, J, K, L, M, N, R, T, and Y.
+
+Implement the `randomPlanetClass()` function.
+It should return one of the planetary classes at random.
+
+```javascript
+randomPlanetClass();
+// => "K"
+```
diff --git a/exercises/concept/captains-log/.docs/introduction.md b/exercises/concept/captains-log/.docs/introduction.md
new file mode 100644
index 0000000000..cfa1b59484
--- /dev/null
+++ b/exercises/concept/captains-log/.docs/introduction.md
@@ -0,0 +1,26 @@
+# Introduction
+
+Many programs need (pseudo-)random values to simulate real-world events.
+
+Common, familiar examples include:
+
+- A coin toss: a random value from ('H', 'T').
+- The roll of a die: a random integer from 1 to 6.
+- Shuffling a deck of cards: a random ordering of a card list.
+- The creation of trees and bushes in a 3-D graphics simulation.
+
+Generating truly random values with a computer is a [surprisingly difficult technical challenge][why-randomness-is-hard], which is why there are also "pseudorandom" generators.
+
+
+~~~exercism/caution
+The `Math.random()` function should NOT be used for security and cryptographic applications!
+Finish the learning exercise(s) about this concept to learn more
+~~~
+
+## Generating random numbers
+
+In Javascript, you can generate pseudorandom numbers using the [`Math.random()`][Math.random] function.
+It will return a pseudorandom floating-point number between 0 (inclusive), and 1 (exclusive).
+
+[why-randomness-is-hard]: https://www.malwarebytes.com/blog/news/2013/09/in-computers-are-random-numbers-really-random
+[Math.random]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random
diff --git a/exercises/concept/captains-log/.gitignore b/exercises/concept/captains-log/.gitignore
new file mode 100644
index 0000000000..0c88ff6ec3
--- /dev/null
+++ b/exercises/concept/captains-log/.gitignore
@@ -0,0 +1,5 @@
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/captains-log/.meta/config.json b/exercises/concept/captains-log/.meta/config.json
new file mode 100644
index 0000000000..9a9cff01f5
--- /dev/null
+++ b/exercises/concept/captains-log/.meta/config.json
@@ -0,0 +1,20 @@
+{
+ "authors": [
+ "SneakyMallard"
+ ],
+ "contributors": [
+ "SleeplessByte"
+ ],
+ "files": {
+ "solution": [
+ "captains-log.js"
+ ],
+ "test": [
+ "captains-log.spec.js"
+ ],
+ "exemplar": [
+ ".meta/exemplar.js"
+ ]
+ },
+ "blurb": "Learn about randomness and the Math.random() function while helping Mary generate stardates and starship registry numbers for her Star Trek roleplay."
+}
diff --git a/exercises/concept/captains-log/.meta/design.md b/exercises/concept/captains-log/.meta/design.md
new file mode 100644
index 0000000000..fcf0cc49f9
--- /dev/null
+++ b/exercises/concept/captains-log/.meta/design.md
@@ -0,0 +1,26 @@
+# Design
+
+## Goal
+
+The goal of this exercise is to teach the student how to generate pseudorandom numbers in JavaScript.
+
+## Learning objectives
+
+- Know how to generate a random number with `Math.random()`
+- Know how to generate a random number in a range.
+- Know how to generate a random integer.
+
+## Out of scope
+
+- Details of pseudorandom number generation in general.
+- Different algorithms for pseudorandom number generation.
+
+## Concepts
+
+The Concepts this exercise unlocks are:
+
+- `randomness`: Know of the `Math.random()` function and know how to use it to generate random numbers.
+
+## Prerequisites
+
+- `numbers`: Know how numbers work in JavaScript. Know some number methods.
diff --git a/exercises/concept/captains-log/.meta/exemplar.js b/exercises/concept/captains-log/.meta/exemplar.js
new file mode 100644
index 0000000000..0488199d31
--- /dev/null
+++ b/exercises/concept/captains-log/.meta/exemplar.js
@@ -0,0 +1,30 @@
+// @ts-check
+
+/**
+ * Generates a random starship registry number.
+ *
+ * @returns {string} the generated registry number.
+ */
+export function randomShipRegistryNumber() {
+ return `NCC-${Math.floor(1000 + Math.random() * 9000)}`;
+}
+
+/**
+ * Generates a random stardate.
+ *
+ * @returns {number} a stardate between 41000 (inclusive) and 42000 (exclusive).
+ */
+export function randomStardate() {
+ return 41000 + Math.random() * 1000;
+}
+
+const PLANET_CLASSES = 'DHJKLMNRTY';
+
+/**
+ * Generates a random planet class.
+ *
+ * @returns {string} a one-letter planet class.
+ */
+export function randomPlanetClass() {
+ return PLANET_CLASSES[Math.floor(Math.random() * PLANET_CLASSES.length)];
+}
diff --git a/exercises/concept/captains-log/.npmrc b/exercises/concept/captains-log/.npmrc
new file mode 100644
index 0000000000..d26df800bb
--- /dev/null
+++ b/exercises/concept/captains-log/.npmrc
@@ -0,0 +1 @@
+audit=false
diff --git a/exercises/concept/captains-log/LICENSE b/exercises/concept/captains-log/LICENSE
new file mode 100644
index 0000000000..90e73be03b
--- /dev/null
+++ b/exercises/concept/captains-log/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Exercism
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/exercises/concept/captains-log/babel.config.js b/exercises/concept/captains-log/babel.config.js
new file mode 100644
index 0000000000..a638497df1
--- /dev/null
+++ b/exercises/concept/captains-log/babel.config.js
@@ -0,0 +1,4 @@
+module.exports = {
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
+ plugins: [],
+};
diff --git a/exercises/concept/captains-log/captains-log.js b/exercises/concept/captains-log/captains-log.js
new file mode 100644
index 0000000000..978cff64fd
--- /dev/null
+++ b/exercises/concept/captains-log/captains-log.js
@@ -0,0 +1,34 @@
+// @ts-check
+
+/**
+ * Generates a random starship registry number.
+ *
+ * @returns {string} the generated registry number.
+ */
+export function randomShipRegistryNumber() {
+ throw new Error(
+ 'Please remove this line and implement the randomShipRegistryNumber() function',
+ );
+}
+
+/**
+ * Generates a random stardate.
+ *
+ * @returns {number} a stardate between 41000 (inclusive) and 42000 (exclusive).
+ */
+export function randomStardate() {
+ throw new Error(
+ 'Please remove this line and implement the randomStardate() function',
+ );
+}
+
+/**
+ * Generates a random planet class.
+ *
+ * @returns {string} a one-letter planet class.
+ */
+export function randomPlanetClass() {
+ throw new Error(
+ 'Please remove this line and implement the randomStardate() function',
+ );
+}
diff --git a/exercises/concept/captains-log/captains-log.spec.js b/exercises/concept/captains-log/captains-log.spec.js
new file mode 100644
index 0000000000..5cfd79d0bb
--- /dev/null
+++ b/exercises/concept/captains-log/captains-log.spec.js
@@ -0,0 +1,77 @@
+import { describe, expect, test } from '@jest/globals';
+import {
+ randomShipRegistryNumber,
+ randomStardate,
+ randomPlanetClass,
+} from './captains-log';
+
+describe('randomShipRegistryNumber', () => {
+ test('registry numbers are valid', () => {
+ for (let i = 0; i < 4; i++) {
+ expect(randomShipRegistryNumber()).toMatch(/NCC-[1-9][0-9]{3}/);
+ }
+ });
+
+ test('returns a random registry number', () => {
+ expect(randomShipRegistryNumber()).not.toEqual(randomShipRegistryNumber());
+ });
+});
+
+function loadDie(...values) {
+ const originalRandom = Math.random;
+
+ Math.random = function loadedDie() {
+ if (values.length === 0) {
+ return originalRandom();
+ }
+ return values.shift();
+ };
+
+ return () => {
+ Math.random = originalRandom;
+ };
+}
+
+describe('randomStardate', () => {
+ test('stardate is between 41000 and 42000', () => {
+ const min = 0;
+ const max = 1 - Number.EPSILON * 32;
+
+ // prettier-ignore
+ const restore = loadDie(
+ min, min, min, min, min, min,
+ max, max, max, max, max, max,
+ 0.5, 0.5, 0.5, 0.5, 0.5, 0.5
+ );
+
+ for (let i = 0; i < 10_000; i++) {
+ const starDate = randomStardate();
+ expect(starDate).toBeGreaterThanOrEqual(41_000);
+ expect(starDate).toBeLessThan(42_000);
+ }
+
+ restore();
+ });
+});
+
+describe('randomPlanetClass', () => {
+ test('planet classes are valid', () => {
+ const expected = 'DHJKLMNRTY';
+ for (let i = 0; i < 1_000; i++) {
+ const actual = randomPlanetClass();
+ expect(expected).toContain(actual);
+ }
+ });
+
+ test('all planet classes can be returned', () => {
+ const expected = 'DHJKLMNRTY';
+ const seen = {};
+
+ for (let i = 0; i < 1_000; i++) {
+ const actual = randomPlanetClass();
+ seen[actual] = true;
+ }
+
+ expect(Object.keys(seen).length).toBe(expected.length);
+ });
+});
diff --git a/exercises/concept/captains-log/eslint.config.mjs b/exercises/concept/captains-log/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/captains-log/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/captains-log/jest.config.js b/exercises/concept/captains-log/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/captains-log/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/captains-log/package.json b/exercises/concept/captains-log/package.json
new file mode 100644
index 0000000000..8c46cfe06c
--- /dev/null
+++ b/exercises/concept/captains-log/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "@exercism/javascript-captains-log",
+ "description": "Exercism concept exercise on randomness",
+ "author": "J R M (https://github.com/quintuple-mallard)",
+ "contributors": [
+ "Derk-Jan Karrenbeld (https://derk-jan.com)",
+ "Cool-Katt (https://github.com/Cool-Katt)"
+ ],
+ "private": true,
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/exercism/javascript",
+ "directory": "exercises/concept/captains-log"
+ },
+ "devDependencies": {
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
+ },
+ "dependencies": {},
+ "scripts": {
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
+}
diff --git a/exercises/concept/coordinate-transformation/.docs/instructions.md b/exercises/concept/coordinate-transformation/.docs/instructions.md
index f01ddba7be..727589a705 100644
--- a/exercises/concept/coordinate-transformation/.docs/instructions.md
+++ b/exercises/concept/coordinate-transformation/.docs/instructions.md
@@ -8,6 +8,9 @@ so you decide to use a function closure to create reusable transformation for `{
Implement the `translate2d` function that returns a function making use of a closure to perform a repeatable 2d translation of a coordinate pair.
+> In Geometry, [translation][wiki-translate] refers to moving points, vectors or shapes the same distance in one direction.
+> It can be interpreted as addition of a constant to every point.
+
```javascript
const moveCoordinatesRight2Px = translate2d(2, 0);
const result = moveCoordinatesRight2Px(4, 8);
@@ -18,6 +21,9 @@ const result = moveCoordinatesRight2Px(4, 8);
Implement the `scale2d` function that returns a function making use of a closure to perform a repeatable 2d scale of a coordinate pair.
+> In geometry, uniform [scaling][wiki-scale] refers to enlarging or shrinking vectors or shapes in the same direction.
+> It can be interpreted as multiplying every point by a constant (scaling factor).
+>
> For this exercise, assume only positive scaling values.
```javascript
@@ -33,9 +39,9 @@ Combine two transformation functions to perform a repeatable transformation. Thi
```javascript
const moveCoordinatesRight2Px = translate2d(2, 0);
const doubleCoordinates = scale2d(2, 2);
-const composedTransformations = composeTransformation(
+const composedTransformations = composeTransform(
moveCoordinatesRight2Px,
- doubleCoordinates
+ doubleCoordinates,
);
const result = composedTransformations(0, 1);
// result => [4, 2]
@@ -56,3 +62,6 @@ const memoizedScale = memoizeTransform(tripleScale);
memoizedScale(4, 3); // => [12, 9], this is computed since it hasn't been computed before for the arguments
memoizedScale(4, 3); // => [12, 9], this is remembered, since it was computed already
```
+
+[wiki-translate]: https://en.wikipedia.org/wiki/Translation_(geometry)
+[wiki-scale]: https://en.wikipedia.org/wiki/Scaling_(geometry)
diff --git a/exercises/concept/coordinate-transformation/.docs/introduction.md b/exercises/concept/coordinate-transformation/.docs/introduction.md
index 7af38ff52e..db1e434ff6 100644
--- a/exercises/concept/coordinate-transformation/.docs/introduction.md
+++ b/exercises/concept/coordinate-transformation/.docs/introduction.md
@@ -1,6 +1,7 @@
# Introduction
-**Closures** are a programming pattern in JavaScript which allows variables from an outer [lexical scope][wiki-lexical-scope] to be used inside of a nested block of code. JavaScript supports closures transparently, and they are often used without knowing what they are.
+**Closures** are a programming pattern in JavaScript which allows variables from an outer [lexical scope][wiki-lexical-scope] to be used inside of a nested block of code.
+JavaScript supports closures transparently, and they are often used without knowing what they are.
```javascript
// Top-level declarations are global-scope
@@ -12,13 +13,35 @@ const dozen = 12;
const twoDozen = dozen * 2;
}
+// Because of the block-scope declaration, twoDozen is not available here.
+twoDozen;
+// => Uncaught ReferenceError: twoDozen is not defined
+```
+
+Besides braces `{}`, functions (and classes) also create new scopes, and can _enclose_ values:
+
+```javascript
+const dozen = 12;
+
// Functions create a new function-scope and block-scope.
// Referencing the outer variable here is a closure.
function nDozen(n) {
- return dozen * n;
+ // This is declared inside the function scope, and uses the top-level scope.
+ // This works, and encloses the value 12.
+ const twoDozen = dozen * 2;
+
+ // This only uses the locally declared variable and the passed argument to the parameter `n`
+ return (twoDozen / 2) * n;
}
+
+// Because of the function-scope declaration, twoDozen is not available here.
+twoDozen;
+// => Uncaught ReferenceError: twoDozen is not defined
```
+As the `twoDozen` examples show, values can be enclosed in a _nested_ scope (function, block, etc.), but cannot be pulled out of that context.
+In the majority of cases, it is intended in Modern JavaScript that a value does not _leak_ to an outside scope.
+
## Closures to save state and pass along values
Using a mutable variable declaration (like `let` or `var`) allows for some state to be preserved:
@@ -32,6 +55,67 @@ export function increment() {
counter += 1;
return counter;
}
+
+increment();
+// => 1
+
+counter;
+// => 1
+```
+
+## Enclosing values without leaking the state
+
+Combining the two ideas: enclosing a value to preserve state, and enclosed values do not leak to the outside, it's possible to create private values.
+
+The most common method is to make a function that returns a function which encloses some state.
+
+```javascript
+export function makeCounter() {
+ let counter = 0;
+
+ // This returns a new function that encloses the local variable counter
+ return function increment() {
+ counter += 1;
+ return counter;
+ };
+}
+
+// Counter did not leak
+counter;
+// => Uncaught ReferenceError: counter is not defined
+
+// This creates a new counter.
+// This assigns the increment function to the variable myFirstCounter.
+const myFirstCounter = makeCounter();
+
+typeof myFirstCounter;
+// => function
+
+myFirstCounter.name;
+// => increment
+
+myFirstCounter();
+// => 1
+myFirstCounter();
+// => 2
+
+// This creates new counter (with new, separate local state / enclosed counter variable)
+const mySecondCounter = makeCounter();
+
+mySecondCounter();
+// => 1
+
+// It does not affect the first counter.
+
+myFirstCounter();
+// => 3
+```
+
+```exercism/note
+Many programmers find closures a hard concept, and returning a function from a function is not common or not even possible in all programming languages.
+If you want more reading material, the [guide on MDN on Closures][mdn-closures] is quite comprehensive.
+
+[mdn-closures]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures
```
[wiki-lexical-scope]: https://en.wikipedia.org/wiki/Scope_(computer_science)#Lexical_scoping
diff --git a/exercises/concept/coordinate-transformation/.eslintrc b/exercises/concept/coordinate-transformation/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/concept/coordinate-transformation/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/concept/coordinate-transformation/.gitignore b/exercises/concept/coordinate-transformation/.gitignore
index bdb912f98a..0c88ff6ec3 100644
--- a/exercises/concept/coordinate-transformation/.gitignore
+++ b/exercises/concept/coordinate-transformation/.gitignore
@@ -1,3 +1,5 @@
-node_modules
-yarn-error.log
-
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/coordinate-transformation/.meta/config.json b/exercises/concept/coordinate-transformation/.meta/config.json
index 978f9b9544..76b4e5a024 100644
--- a/exercises/concept/coordinate-transformation/.meta/config.json
+++ b/exercises/concept/coordinate-transformation/.meta/config.json
@@ -3,7 +3,8 @@
"neenjaw"
],
"contributors": [
- "SleeplessByte"
+ "SleeplessByte",
+ "meonkeys"
],
"files": {
"solution": [
diff --git a/exercises/concept/coordinate-transformation/babel.config.js b/exercises/concept/coordinate-transformation/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/concept/coordinate-transformation/babel.config.js
+++ b/exercises/concept/coordinate-transformation/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/concept/coordinate-transformation/coordinate-transformation.js b/exercises/concept/coordinate-transformation/coordinate-transformation.js
index e32567ad1a..561faa2d3e 100644
--- a/exercises/concept/coordinate-transformation/coordinate-transformation.js
+++ b/exercises/concept/coordinate-transformation/coordinate-transformation.js
@@ -4,6 +4,10 @@
// the @ts-check directive. It will give you helpful autocompletion when
// implementing this exercise.
+/**
+ * @typedef {function (number, number): [number, number]} CallbackType
+ */
+
/**
* Create a function that returns a function making use of a closure to
* perform a repeatable 2d translation of a coordinate pair.
@@ -11,11 +15,11 @@
* @param {number} dx the translate x component
* @param {number} dy the translate y component
*
- * @returns {function} a function which takes an x, y parameter, returns the
+ * @returns {CallbackType} a function which takes an x, y parameter, returns the
* translated coordinate pair in the form [x, y]
*/
export function translate2d(dx, dy) {
- throw new Error('Implement the translate2d function');
+ throw new Error('Remove this line and implement the function');
}
/**
@@ -25,36 +29,37 @@ export function translate2d(dx, dy) {
* @param {number} sx the amount to scale the x component
* @param {number} sy the amount to scale the y component
*
- * @returns {function} a function which takes an x, y parameter, returns the
+ * @returns {CallbackType} a function which takes an x, y parameter, returns the
* scaled coordinate pair in the form [x, y]
*/
export function scale2d(sx, sy) {
- throw new Error('Implement the scale2d function');
+ throw new Error('Remove this line and implement the function');
}
/**
* Create a composition function that returns a function that combines two
* functions to perform a repeatable transformation
*
- * @param {function} f the first function to apply
- * @param {function} g the second function to apply
+ * @param {CallbackType} f the first function to apply
+ * @param {CallbackType} g the second function to apply
*
- * @returns {function} a function which takes an x, y parameter, returns the
+ * @returns {CallbackType} a function which takes an x, y parameter, returns the
* transformed coordinate pair in the form [x, y]
*/
export function composeTransform(f, g) {
- throw new Error('Implement the composeTransform function');
+ throw new Error('Remove this line and implement the function');
}
/**
* Return a function that memoizes the last result. If the arguments are the same as the last call,
* then memoized result returned.
*
- * @param {function} f the transformation function to memoize, assumes takes two arguments 'x' and 'y'
+ * @param {CallbackType} f the transformation function to memoize, assumes takes two arguments 'x' and 'y'
*
- * @returns {function} a function which takes x and y arguments, and will either return the saved result
+ * @returns {CallbackType} a function which takes x and y arguments, and will either return the saved result
* if the arguments are the same on subsequent calls, or compute a new result if they are different.
*/
export function memoizeTransform(f) {
- throw new Error('Implement the memoizeTransform function');
+ // To narrow the type of the return variable, add `/** @type {[number, number]} */` above it.
+ throw new Error('Remove this line and implement the function');
}
diff --git a/exercises/concept/coordinate-transformation/coordinate-transformation.spec.js b/exercises/concept/coordinate-transformation/coordinate-transformation.spec.js
index 9eb4b5bd1c..f548002ca0 100644
--- a/exercises/concept/coordinate-transformation/coordinate-transformation.spec.js
+++ b/exercises/concept/coordinate-transformation/coordinate-transformation.spec.js
@@ -1,8 +1,9 @@
+import { describe, expect, test, jest } from '@jest/globals';
import {
- translate2d,
- scale2d,
composeTransform,
memoizeTransform,
+ scale2d,
+ translate2d,
} from './coordinate-transformation';
const fakeTransform = () => {
@@ -25,11 +26,11 @@ describe('translate2d', () => {
const dx = 3;
const dy = -5;
- const translator = translate2d(dx, dy);
const x1 = 0;
const y1 = 0;
const expected = [3, -5];
test('should be predictable', () => {
+ const translator = translate2d(dx, dy);
expect(translator(x1, y1)).toEqual(expected);
});
@@ -37,6 +38,8 @@ describe('translate2d', () => {
const y2 = 5;
const reusedExpected = [7, 0];
test('should be reusable', () => {
+ const translator = translate2d(dx, dy);
+ translator(x1, y1);
expect(translator(x2, y2)).toEqual(reusedExpected);
});
});
@@ -48,11 +51,11 @@ describe('scale2d', () => {
const dx = 4;
const dy = 2;
- const scaler = scale2d(dx, dy);
const x1 = 1;
const y1 = 1;
const expected = [4, 2];
test('should be predictable', () => {
+ const scaler = scale2d(dx, dy);
expect(scaler(x1, y1)).toEqual(expected);
});
@@ -60,6 +63,8 @@ describe('scale2d', () => {
const y2 = 5;
const reusedExpected = [-8, 10];
test('should be reusable', () => {
+ const scaler = scale2d(dx, dy);
+ scaler(x1, y1);
expect(scaler(x2, y2)).toEqual(reusedExpected);
});
});
@@ -67,31 +72,37 @@ describe('scale2d', () => {
describe('composeTransform', () => {
const dx = -6;
const dy = 10;
- const translator = translate2d(dx, dy);
const sx = 3;
const sy = 2;
- const scaler = scale2d(sx, sy);
test('should return a function', () => {
+ const translator = translate2d(dx, dy);
+ const scaler = scale2d(sx, sy);
expect(typeof composeTransform(translator, scaler)).toBe('function');
});
test('should compose two translate functions', () => {
+ const translator = translate2d(dx, dy);
const composeTranslate = composeTransform(translator, translator);
expect(composeTranslate(0, 0)).toEqual([-12, 20]);
});
test('should compose two scale functions', () => {
+ const scaler = scale2d(sx, sy);
const composeScale = composeTransform(scaler, scaler);
expect(composeScale(1, 1)).toEqual([9, 4]);
});
test('should compose in the correct order: g(f(x))', () => {
+ const translator = translate2d(dx, dy);
+ const scaler = scale2d(sx, sy);
const composed = composeTransform(scaler, translator);
expect(composed(0, 0)).toEqual([-6, 10]);
});
- test('should compose in the opposite order: g(f(x))', () => {
+ test('should compose in the opposite order: f(g(x))', () => {
+ const translator = translate2d(dx, dy);
+ const scaler = scale2d(sx, sy);
const composed = composeTransform(translator, scaler);
expect(composed(0, 0)).toEqual([-18, 20]);
});
@@ -111,6 +122,7 @@ describe('memoizeTransform', () => {
test('should return different results for different inputs', () => {
const memoizedTranslate = memoizeTransform(translate2d(1, 2));
expect(memoizedTranslate(2, 2)).toEqual([3, 4]);
+ expect(memoizedTranslate(2, 1)).toEqual([3, 3]);
expect(memoizedTranslate(6, 6)).toEqual([7, 8]);
});
@@ -126,6 +138,16 @@ describe('memoizeTransform', () => {
expect(memoizedTransform(1, 1)).toEqual([2, 2]);
expect(memoizedTransform(2, 2)).toEqual([4, 4]);
expect(memoizedTransform(1, 1)).toEqual([2, 2]);
- expect(mockFunction).toBeCalledTimes(3);
+ expect(mockFunction).toHaveBeenCalledTimes(3);
+ });
+
+ test('should recalculate when a new function is passed in', () => {
+ const sumFunction = (x, y) => x + y;
+ const differenceFunction = (x, y) => x - y;
+ const memoizedSum = memoizeTransform(sumFunction);
+ const memoizedDifference = memoizeTransform(differenceFunction);
+
+ expect(memoizedSum(1, 2)).toEqual(3);
+ expect(memoizedDifference(1, 2)).toEqual(-1);
});
});
diff --git a/exercises/concept/coordinate-transformation/eslint.config.mjs b/exercises/concept/coordinate-transformation/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/coordinate-transformation/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/coordinate-transformation/jest.config.js b/exercises/concept/coordinate-transformation/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/coordinate-transformation/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/coordinate-transformation/package.json b/exercises/concept/coordinate-transformation/package.json
index d3dbcab2fd..1fd8cc00f6 100644
--- a/exercises/concept/coordinate-transformation/package.json
+++ b/exercises/concept/coordinate-transformation/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/concept/coordinate-transformation"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/concept/custom-signs/.docs/instructions.md b/exercises/concept/custom-signs/.docs/instructions.md
index 3635a2d8cb..e4ccb0ba46 100644
--- a/exercises/concept/custom-signs/.docs/instructions.md
+++ b/exercises/concept/custom-signs/.docs/instructions.md
@@ -4,7 +4,8 @@ In this exercise you'll be writing code to help a sign company create custom mes
## 1. Build an occasion sign
-Implement the function `buildSign(occasion, name)` that accepts a string as the `occasion` parameter and a string holding someone's name as the `name` parameter. The two parameters will be embedded into a template string to output the message on the sign.
+Implement the function `buildSign(occasion, name)` that accepts a string as the `occasion` parameter and a string holding someone's name as the `name` parameter.
+The two parameters will be embedded into a template string to output the message on the sign.
```javascript
buildSign('Birthday', 'Rob');
@@ -13,9 +14,14 @@ buildSign('Birthday', 'Rob');
## 2. Build a birthday sign
-Implement the function `buildBirthdaySign(age)` that accepts an age and based on the age will determine part of the message on the sign. If the age is 50 or older, the sign will include the word _mature_, otherwise the sign will include the word _young_.
+Implement the function `buildBirthdaySign(age)` that accepts an age and based on the age will determine part of the message on the sign.
+If the age is 50 or older, the sign will refer user as _mature_, else it will refer them as _young_.
+The exact expected output is shown below:
```javascript
+buildBirthdaySign(50);
+// => "Happy Birthday! What a mature fellow you are."
+
buildBirthdaySign(45);
// => "Happy Birthday! What a young fellow you are."
```
@@ -26,7 +32,9 @@ Implement the function `graduationFor(name, year)` which takes a name as a strin
```javascript
graduationFor('Hannah', 2022);
-// => "Congratulations Hannah!\nClass of 2022"
+/* => "Congratulations Hannah!
+ Class of 2022"
+*/
```
## 4. Compute the cost of a sign
diff --git a/exercises/concept/custom-signs/.eslintrc b/exercises/concept/custom-signs/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/concept/custom-signs/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/concept/custom-signs/.gitignore b/exercises/concept/custom-signs/.gitignore
index bdb912f98a..0c88ff6ec3 100644
--- a/exercises/concept/custom-signs/.gitignore
+++ b/exercises/concept/custom-signs/.gitignore
@@ -1,3 +1,5 @@
-node_modules
-yarn-error.log
-
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/custom-signs/babel.config.js b/exercises/concept/custom-signs/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/concept/custom-signs/babel.config.js
+++ b/exercises/concept/custom-signs/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/concept/custom-signs/custom-signs.js b/exercises/concept/custom-signs/custom-signs.js
index 26d729bc9b..a05dcc934f 100644
--- a/exercises/concept/custom-signs/custom-signs.js
+++ b/exercises/concept/custom-signs/custom-signs.js
@@ -14,7 +14,7 @@
*/
export function buildSign(occasion, name) {
- throw new Error('Implement the buildSign function');
+ throw new Error('Remove this line and implement the function');
}
/**
@@ -26,7 +26,7 @@ export function buildSign(occasion, name) {
*/
export function buildBirthdaySign(age) {
- throw new Error('Implement the buildBirthdaySign function');
+ throw new Error('Remove this line and implement the function');
}
/**
@@ -39,7 +39,7 @@ export function buildBirthdaySign(age) {
*/
export function graduationFor(name, year) {
- throw new Error('Implement the graduationForm function');
+ throw new Error('Remove this line and implement the function');
}
/**
@@ -53,5 +53,5 @@ export function graduationFor(name, year) {
*/
export function costOf(sign, currency) {
- throw new Error('Implement the costOf function');
+ throw new Error('Remove this line and implement the function');
}
diff --git a/exercises/concept/custom-signs/custom-signs.spec.js b/exercises/concept/custom-signs/custom-signs.spec.js
index 36c838144a..3c805c909e 100644
--- a/exercises/concept/custom-signs/custom-signs.spec.js
+++ b/exercises/concept/custom-signs/custom-signs.spec.js
@@ -1,10 +1,9 @@
-//@ts-check
-
+import { describe, expect, test } from '@jest/globals';
import {
- buildSign,
buildBirthdaySign,
- graduationFor,
+ buildSign,
costOf,
+ graduationFor,
} from './custom-signs';
describe('buildSign', () => {
@@ -20,19 +19,19 @@ describe('buildSign', () => {
describe('buildBirthdaySign', () => {
test('age is less than 50', () => {
expect(buildBirthdaySign(49)).toBe(
- 'Happy Birthday! What a young fellow you are.'
+ 'Happy Birthday! What a young fellow you are.',
);
});
test('age is 50 or older', () => {
expect(buildBirthdaySign(51)).toBe(
- 'Happy Birthday! What a mature fellow you are.'
+ 'Happy Birthday! What a mature fellow you are.',
);
});
test('age is 50', () => {
expect(buildBirthdaySign(50)).toBe(
- 'Happy Birthday! What a mature fellow you are.'
+ 'Happy Birthday! What a mature fellow you are.',
);
});
});
diff --git a/exercises/concept/custom-signs/eslint.config.mjs b/exercises/concept/custom-signs/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/custom-signs/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/custom-signs/jest.config.js b/exercises/concept/custom-signs/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/custom-signs/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/custom-signs/package.json b/exercises/concept/custom-signs/package.json
index eed4d1978f..8da71efeed 100644
--- a/exercises/concept/custom-signs/package.json
+++ b/exercises/concept/custom-signs/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/concept/custom-signs"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/concept/elyses-analytic-enchantments/.docs/hints.md b/exercises/concept/elyses-analytic-enchantments/.docs/hints.md
index d3388ae43c..4d370f4a0c 100644
--- a/exercises/concept/elyses-analytic-enchantments/.docs/hints.md
+++ b/exercises/concept/elyses-analytic-enchantments/.docs/hints.md
@@ -28,6 +28,6 @@
[indexof_method_docs]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf
[includes_method_docs]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes
[every_method_docs]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every
-[some_method_docs]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every
+[some_method_docs]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some
[find_method_docs]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find
[findindex_method_docs]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex
diff --git a/exercises/concept/elyses-analytic-enchantments/.docs/introduction.md b/exercises/concept/elyses-analytic-enchantments/.docs/introduction.md
index ed88634f7a..489b79e710 100644
--- a/exercises/concept/elyses-analytic-enchantments/.docs/introduction.md
+++ b/exercises/concept/elyses-analytic-enchantments/.docs/introduction.md
@@ -63,11 +63,13 @@ numbers.indexOf('two');
// => 1
```
-Some other helpful built-in methods that are available to analyze an array are shown below. See [MDN][mdn-array-methods] for a full list of array methods.
+Some other helpful built-in methods that are available to analyze an array are shown below.
+See [MDN][mdn-array-methods] for a full list of array methods.
### `includes`
-> The includes() method determines whether an array includes a certain value among its entries, returning true or false as appropriate. [^1]
+The `includes(value)` method determines whether an array includes a certain value.
+It returns `true` when the value is included, `false` otherwise.
```javascript
const numbers = [1, 'two', 3, 'four'];
@@ -79,7 +81,10 @@ numbers.includes('one');
### `every`
-> The every() method tests whether all elements in the array pass the test implemented by the provided function. It returns a Boolean value. [^2]
+The `every(predicate)` method take a function which is _a predicate_.
+It tests whether all elements in the array return `true` when passed to the predicate.
+In other words: the methods tests that all its elements pass the test passed to the function call.
+It returns `true` when every element passes the predicate test, `false` otherwise.
```javascript
const numbers = [1, 3, 5, 7, 9];
@@ -89,7 +94,7 @@ numbers.every((num) => num % 2 !== 0);
### `some`
-> The some() method tests whether at least one element in the array passes the test implemented by the provided function. [^3]
+The `some(predicate)` method is the same as the `every` method, but returns `true` if at least one item in the array passes the _predicate_ test.
```javascript
const numbers = [1, 3, 5, 7, 9];
@@ -99,7 +104,9 @@ numbers.some((num) => num % 2 !== 0);
### `find`
-> The find() method returns the value of the first element in the provided array that satisfies the provided testing function. If no values satisfy the testing function, undefined is returned. [^4]
+The `find(predicate)` method returns the value of the first element in the array that passes the `predicate` test.
+Where `some()` returns `true` when it passes, `find()` returns the actual value in the array.
+The method returns `undefined` when none of the elements in the array pass the _predicate_ test.
```javascript
const numbers = [1, 3, 5, 7, 9];
@@ -109,7 +116,8 @@ numbers.find((num) => num < 5);
### `findIndex`
-> The findIndex() method returns the index of the first element in the array that satisfies the provided testing function. Otherwise, it returns -1, indicating that no element passed the test. [^5]
+The `findIndex(predicate)` is the same as the `find()` method, but it returns the (first) _index_ of the element that passes the _predicate_ test instead of the `value`.
+The method returns `-1` when none of the elements in the array pass the _predicate_ test.
```javascript
const numbers = [1, 3, 5, 7, 9];
@@ -119,12 +127,6 @@ numbers.findIndex((num) => num > 9);
// => -1
```
-[^1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes
-[^2]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every
-[^3]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some
-[^4]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find
-[^5]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex
-
[predicate_in_programming]: https://derk-jan.com/2020/05/predicate/
[mdn-array-methods]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#instance_methods
[arrays-concept]: /tracks/javascript/concepts/arrays
diff --git a/exercises/concept/elyses-analytic-enchantments/.eslintrc b/exercises/concept/elyses-analytic-enchantments/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/concept/elyses-analytic-enchantments/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/concept/elyses-analytic-enchantments/.gitignore b/exercises/concept/elyses-analytic-enchantments/.gitignore
index bdb912f98a..0c88ff6ec3 100644
--- a/exercises/concept/elyses-analytic-enchantments/.gitignore
+++ b/exercises/concept/elyses-analytic-enchantments/.gitignore
@@ -1,3 +1,5 @@
-node_modules
-yarn-error.log
-
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/elyses-analytic-enchantments/.meta/exemplar.js b/exercises/concept/elyses-analytic-enchantments/.meta/exemplar.js
index 606292ba62..bd7f3cac8a 100644
--- a/exercises/concept/elyses-analytic-enchantments/.meta/exemplar.js
+++ b/exercises/concept/elyses-analytic-enchantments/.meta/exemplar.js
@@ -51,7 +51,7 @@ export function doesStackIncludeOddCard(stack) {
*
* @param {number[]} stack
*
- * @returns {number} the first odd value
+ * @returns {number | undefined} the first odd value
*/
export function getFirstOddCard(stack) {
return stack.find((card) => card % 2 !== 0);
diff --git a/exercises/concept/elyses-analytic-enchantments/babel.config.js b/exercises/concept/elyses-analytic-enchantments/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/concept/elyses-analytic-enchantments/babel.config.js
+++ b/exercises/concept/elyses-analytic-enchantments/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/concept/elyses-analytic-enchantments/enchantments.js b/exercises/concept/elyses-analytic-enchantments/enchantments.js
index 36a3c8caca..60a7c30eb6 100644
--- a/exercises/concept/elyses-analytic-enchantments/enchantments.js
+++ b/exercises/concept/elyses-analytic-enchantments/enchantments.js
@@ -55,7 +55,7 @@ export function doesStackIncludeOddCard(stack) {
*
* @param {number[]} stack
*
- * @returns {number} the first odd value
+ * @returns {number | undefined} the first odd value
*/
export function getFirstOddCard(stack) {
throw new Error('Implement the getFirstOddCard function');
diff --git a/exercises/concept/elyses-analytic-enchantments/enchantments.spec.js b/exercises/concept/elyses-analytic-enchantments/enchantments.spec.js
index 295cb21ef3..31e9ee1aae 100644
--- a/exercises/concept/elyses-analytic-enchantments/enchantments.spec.js
+++ b/exercises/concept/elyses-analytic-enchantments/enchantments.spec.js
@@ -1,12 +1,11 @@
-// @ts-check
-
+import { describe, expect, test } from '@jest/globals';
import {
- getCardPosition,
doesStackIncludeCard,
- isEachCardEven,
doesStackIncludeOddCard,
- getFirstOddCard,
+ getCardPosition,
getFirstEvenCardPosition,
+ getFirstOddCard,
+ isEachCardEven,
} from './enchantments';
describe('getCardPosition', () => {
diff --git a/exercises/concept/elyses-analytic-enchantments/eslint.config.mjs b/exercises/concept/elyses-analytic-enchantments/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/elyses-analytic-enchantments/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/elyses-analytic-enchantments/jest.config.js b/exercises/concept/elyses-analytic-enchantments/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/elyses-analytic-enchantments/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/elyses-analytic-enchantments/package.json b/exercises/concept/elyses-analytic-enchantments/package.json
index 1361dfe2d2..f440358e92 100644
--- a/exercises/concept/elyses-analytic-enchantments/package.json
+++ b/exercises/concept/elyses-analytic-enchantments/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/concept/elyses-analytic-enchantments"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/concept/elyses-destructured-enchantments/.docs/hints.md b/exercises/concept/elyses-destructured-enchantments/.docs/hints.md
index bdad5257c2..c6c82baeac 100644
--- a/exercises/concept/elyses-destructured-enchantments/.docs/hints.md
+++ b/exercises/concept/elyses-destructured-enchantments/.docs/hints.md
@@ -2,32 +2,35 @@
## 1. Get the first card
-- [This article][destructuring_overview_resource] has a good overview of array destructuring. You can find an example of basic variable assignment in the 'Basic Array Destructuring' section.
+- [This article][mdn-destructuring] has a good overview of array destructuring. You can find an example of basic variable assignment in the 'Basic Array Destructuring' section.
## 2. Get the second card
- You can use placeholders to ignore one or more values in the array.
-- You can find an example [here][ignoring_some_values_resource].
+- You can find an example [on MDN][mdn-destructuring-ignore-value].
-## 3. Swap the first two cards
+## 3. Swap the two cards
- It's possible to swap two values in a single destructuring expression.
-- You can find an example [here][swapping_variables_resource].
+- You can find an example [on MDN][mdn-destructuring-swapping].
-## 4. Discard the top card
+## 4. Shift three cards around
-- There is a [built-in][rest_operator_docs] operator that can be used to collect the remaining values in an array into a single variable.
-- You can find an example [here][rest_assignment_resource].
+- It's possible to change the position of three values in a single destructuring expression.
+- This is the same as swapping two values, but then with three (or more).
-## 5. Insert face cards
+## 5. Pick named pile
-- There is a [built-in][spread_operator_docs] operator that can be used to expand an array into a list.
-- You can find a more detailed overview [here][spread_operator_overview].
+- Objects can be destructured just like arrays.
+- You can find an example [on MDN][mdn-object-destructuring-basic-assignment].
-[destructuring_overview_resource]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Basic_variable_assignment
-[ignoring_some_values_resource]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Ignoring_some_returned_values
-[swapping_variables_resource]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Swapping_variables
-[rest_operator_docs]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#Rest_syntax_parameters
-[rest_assignment_resource]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Assigning_the_rest_of_an_array_to_a_variable
-[spread_operator_docs]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
-[spread_operator_overview]: https://blog.alexdevero.com/javascript-spread-operator
+## 6. Swap named piles
+
+- When a property is extracted from an object, it can be renamed using specific syntax.
+- You can find an example [on MDN][mdn-object-destructuring-object-rename].
+
+[mdn-destructuring]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Basic_variable_assignment
+[mdn-destructuring-ignore-value]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Ignoring_some_returned_values
+[mdn-destructuring-swapping]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Swapping_variables
+[mdn-object-destructuring-basic-assignment]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring#basic_assignment
+[mdn-object-destructuring-object-rename]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring#assigning_to_new_variable_names
diff --git a/exercises/concept/elyses-destructured-enchantments/.docs/instructions.md b/exercises/concept/elyses-destructured-enchantments/.docs/instructions.md
index 640a6ba7a2..57b66c3cc4 100644
--- a/exercises/concept/elyses-destructured-enchantments/.docs/instructions.md
+++ b/exercises/concept/elyses-destructured-enchantments/.docs/instructions.md
@@ -1,12 +1,29 @@
# Instructions
-Elyse, magician-to-be, continues her training. She has a deck of cards she wants to manipulate.
+Elyse, magician-to-be, continues her training.
+She has a deck of cards she wants to manipulate.
-To make things easier, she usually only starts with cards numbered 1 to 10, although some of the tricks may involve additional cards.
+To make things easier, she usually only starts with cards numbered 2 to 10, although some of the tricks may involve additional (face) cards.
+
+
+~~~~exercism/note
+There are **many** ways to shuffle the cards around, but to keep up the illusion of magic, it is vital that *no single method is used*, e.g. Elyse doesn't use `splice`, `slice`, `shift`, `unshift`, `push`, `at`.
+The array accessor `array[index]` and object accessor (`object[key]` and `object.key`) are also never to be used.
+~~~~
+
+
+Want to help Elyse level up her magic?
+
+
+~~~~exercism/advanced
+Every function can be implemented using the parameters and a function body with a single `return expression`.
+~~~~
+
## 1. Get the first card
-Elyse will summon the first card in the deck without using the `array[index]` or `.shift()`. It's just like magic.
+Elyse will summon the first card in the deck without using the `array[index]`, `.at(index)`, or `.shift()`.
+It's just like magic.
```javascript
const deck = [5, 9, 7, 1, 8];
@@ -17,7 +34,7 @@ getFirstCard(deck);
## 2. Get the second card
-Elyse performs sleight of hand and summons the second card in the deck without using the `array[index]`.
+Elyse performs sleight of hand and summons the second card in the deck without using the `array[index]` or `.shift()`.
```javascript
const deck = [3, 2, 10, 6, 7];
@@ -26,35 +43,56 @@ getSecondCard(deck);
// => 2
```
-## 3. Swap the first two cards
+## 3. Swap two cards
+
+Elyse will make the two cards of the deck switch places.
+She doesn't need to call a single function.
+
+```javascript
+const deck = [10, 7];
+
+swapTwoCards(deck);
+// => [7, 10]
+```
+
+## 4. Shift three cards around
-Elyse will make the top two cards of the deck switch places. She doesn't need to call a single function.
+In order to perform some more sleight of hand, Elyse takes three cards and quickly moves the top card to the back, making the middle card the first card and the old bottom card the middle card.
+She doesn't need to call a single function.
```javascript
-const deck = [10, 7, 3, 8, 5];
+const deck = [2, 6, 10];
-swapTopTwoCards(deck);
-// => [7, 10, 3, 8, 5]
+shiftThreeCardsAround(deck);
+// => [6, 10, 2]
```
-## 4. Discard the top card
+## 5. Pick the named pile
-Elyse will separate the deck into two piles. The first pile will contain only the top card of the original deck, while the second pile will contain all the other cards.
+Elyse will separate the deck into two piles.
+She then asks the observer to pick one of the two piles, which we'll name `chosen`.
+The `disregarded` pile is no longer relevant, which she makes disappear.
+She doesn't need to call a single function.
```javascript
-const deck = [2, 5, 4, 9, 3];
+const deck = [5, 4, 7, 10];
+const chosen = [5, 4];
+const disregarded = [7, 10];
-discardTopCard(deck);
-// => [2, [5, 4, 9, 3]]
+pickNamedPile({ chosen, disregarded });
+// => [5, 4]
```
-## 5. Insert face cards
+## 5. Swap the picked pile
-Elyse will insert a set of face cards (i.e. jack, queen, and king) into her deck such that they become the second, third, and fourth cards, respectively.
+Unfortunately the observer keeps picking the "wrong" pile, but with some clever fast magic, Elyse renames the `chosen` pile to be `disregarded` and the `disregarded` pile to be the `chosen` pile.
+She doesn't need to call a single function.
```javascript
const deck = [5, 4, 7, 10];
+const chosen = [5, 4];
+const disregarded = [7, 10];
-insertFaceCards(deck);
-// => [5, 'jack', 'queen', 'king', 4, 7, 10]
+swapNamedPile({ chosen, disregarded });
+// => { chosen: [7, 10], disregarded: [5, 4] }
```
diff --git a/exercises/concept/elyses-destructured-enchantments/.docs/introduction.md b/exercises/concept/elyses-destructured-enchantments/.docs/introduction.md
index b8ee42f346..24117e3b0b 100644
--- a/exercises/concept/elyses-destructured-enchantments/.docs/introduction.md
+++ b/exercises/concept/elyses-destructured-enchantments/.docs/introduction.md
@@ -14,31 +14,45 @@ neptune;
// => 14
```
-## Rest and spread
+In short:
-JavaScript has a built-in `...` operator that makes it easier to work with indefinite numbers of elements. Depending on the context, it's called either a _rest operator_ or _spread operator_.
+- The syntax allows for naming _positioned_ elements in an array, as well as swapping variables using re-assignment.
+- Destructuring syntax is available inside function parameters, and is available on any iterable.
+- Leaving a position unnamed (by not writing _any_ variable name) silently ignores that position.
-### Rest elements
+## Object destructuring
-When `...` appears on the left-hand side of an assignment, those three dots are known as the `rest` operator. The three dots together with a variable name is called a rest element. It collects zero or more values, and stores them into a single array.
+In JavaScript, there is also destructuring syntax to extract properties from an object and assign them to distinct variables.
-```javascript
-const [a, b, ...everythingElse] = [0, 1, 1, 2, 3, 5, 8];
+In this example, weather symbols are extracted from the object `weather`:
-everythingElse;
-// => [1, 2, 3, 5, 8]
+```javascript
+const weather = {
+ sun: '☀️',
+ sun_behind_small_cloud: '🌤️',
+ sun_behind_cloud: '⛅',
+ sun_behind_large_cloud: '🌥️',
+ sun_behind_rain_cloud: '🌦️',
+ cloud: '☁️',
+ cloud_with_rain: '🌧️',
+ cloud_with_snow: '🌨️',
+ cloud_with_lightning: '🌩️',
+ cloud_with_lightning_and_rain: '⛈️',
+};
+
+const { sun, cloud, cloud_with_lightning: thunder } = weather;
+
+sun;
+// => '☀️'
+
+cloud;
+// => '☁️'
+
+thunder;
+// => '🌩️'
```
-Note that in JavaScript, unlike some other languages, a `rest` element cannot have a trailing comma. It _must_ be the last element in a destructuring assignment.
-
-### Spread elements
-
-When `...` appears on the right-hand side of an assignment, it's known as the `spread` operator. It expands an array into a list of elements. Unlike the rest element, it can appear anywhere in an array literal expression, and there can be more than one.
+In short:
-```javascript
-const oneToFive = [1, 2, 3, 4, 5];
-const oneToTen = [...oneToFive, 6, 7, 8, 9, 10];
-
-oneToTen;
-// => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
-```
+- The syntax allows for both extracting properties as well as extracting and renaming them.
+- Destructuring syntax is available inside function parameters.
diff --git a/exercises/concept/elyses-destructured-enchantments/.eslintrc b/exercises/concept/elyses-destructured-enchantments/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/concept/elyses-destructured-enchantments/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/concept/elyses-destructured-enchantments/.gitignore b/exercises/concept/elyses-destructured-enchantments/.gitignore
index bdb912f98a..0c88ff6ec3 100644
--- a/exercises/concept/elyses-destructured-enchantments/.gitignore
+++ b/exercises/concept/elyses-destructured-enchantments/.gitignore
@@ -1,3 +1,5 @@
-node_modules
-yarn-error.log
-
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/elyses-destructured-enchantments/.meta/design.md b/exercises/concept/elyses-destructured-enchantments/.meta/design.md
index f2c20fea65..1db0602844 100644
--- a/exercises/concept/elyses-destructured-enchantments/.meta/design.md
+++ b/exercises/concept/elyses-destructured-enchantments/.meta/design.md
@@ -4,22 +4,19 @@
- Using destructuring to get the first item of an array
- Using destructuring to get the second item of an array (skip hole)
-- Using destructuring + rest elements to get the last item of an array
- Using destructuring to get the first two items of an array
-- Using destructuring + rest elements to get the head and tail of an array
-- Using spread to turn an array into a list of parameters
-- Using rest elements to turn a list of parameters into an array
- Using destructuring to swap two values
+- Using destructuring to extract properties from an object
+- Using destructuring to extract properties from an object and rename them
## Out of scope
-- Anything with objects
- Default values
## Concepts
- `array-destructuring`
-- `rest-and-spread`
+- `object-destructuring`
## Prerequisites
diff --git a/exercises/concept/elyses-destructured-enchantments/.meta/exemplar.js b/exercises/concept/elyses-destructured-enchantments/.meta/exemplar.js
index 3af3244a8d..d92e0aea61 100644
--- a/exercises/concept/elyses-destructured-enchantments/.meta/exemplar.js
+++ b/exercises/concept/elyses-destructured-enchantments/.meta/exemplar.js
@@ -8,9 +8,7 @@
*
* @returns {Card} the first card in the deck
*/
-export function getFirstCard(deck) {
- const [first] = deck;
-
+export function getFirstCard([first]) {
return first;
}
@@ -21,48 +19,53 @@ export function getFirstCard(deck) {
*
* @returns {Card} the second card in the deck
*/
-export function getSecondCard(deck) {
- const [, second] = deck;
-
+export function getSecondCard([, second]) {
return second;
}
/**
- * Switch the position of the first two cards in the given deck
+ * Switch the position of the two cards
*
- * @param {Card[]} deck
+ * @param {[Card, Card]} deck
*
- * @returns {Card[]} new deck with reordered cards
+ * @returns {[Card, Card]} new deck with the 2 cards swapped
*/
-export function swapTopTwoCards([a, b, ...rest]) {
- return [b, a, ...rest];
+export function swapTwoCards([a, b]) {
+ return [b, a];
}
/**
- * Put the top card of the given deck into a separate discard pile
+ * Rotate (shift) the position of the three cards (by one place)
*
- * @param {Card[]} deck
+ * @param {[Card, Card, Card]} deck
*
- * @returns {[Card, Card[]]} the top card of the given
- * deck and a new deck containing all the other cards
+ * @returns {[Card, Card, Card]} new deck with the 3 cards shifted by one position
*/
-export function discardTopCard(deck) {
- const [first, ...rest] = deck;
-
- return [first, rest];
+export function shiftThreeCardsAround([a, b, c]) {
+ return [b, c, a];
}
-/** @type Card[] **/
-const FACE_CARDS = ['jack', 'queen', 'king'];
-
/**
- * Insert face cards into the given deck
+ * Grab the chosen pile from the available piles
*
- * @param {Card[]} deck
+ * @param {{ chosen: Card[], disregarded: Card[] }} piles
+ *
+ * @returns {Card[]} the pile named chosen
+ */
+export function pickNamedPile({ chosen }) {
+ // 🚨 Do NOT write piles.chosen or piles.disregarded.
+ return chosen;
+}
+
+/**
+ * Swap the chosen pile for the disregarded pile and the disregarded pile for the chosen pile
*
- * @returns {Card[]} new deck where the second,
- * third, and fourth cards are the face cards
+ * @param {{ chosen: Card[], disregarded: Card[] }} piles
+ * @returns {{ chosen: Card[], disregarded: Card[] }} new piles where the two piles are swapped
*/
-export function insertFaceCards([head, ...tail]) {
- return [head, ...FACE_CARDS, ...tail];
+export function swapNamedPile({ chosen: disregarded, disregarded: chosen }) {
+ // 🪄 Don't break the magic.
+ // 🚨 Do NOT write piles.chosen or piles.disregarded.
+ // 🚨 Do NOT touch the next line or Elyse will accidentally reveal the trick.
+ return { chosen, disregarded };
}
diff --git a/exercises/concept/elyses-destructured-enchantments/babel.config.js b/exercises/concept/elyses-destructured-enchantments/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/concept/elyses-destructured-enchantments/babel.config.js
+++ b/exercises/concept/elyses-destructured-enchantments/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/concept/elyses-destructured-enchantments/enchantments.js b/exercises/concept/elyses-destructured-enchantments/enchantments.js
index e04735feb8..f88f487877 100644
--- a/exercises/concept/elyses-destructured-enchantments/enchantments.js
+++ b/exercises/concept/elyses-destructured-enchantments/enchantments.js
@@ -24,39 +24,48 @@ export function getSecondCard(deck) {
}
/**
- * Switch the position of the first two cards in the given deck
+ * Switch the position of the two cards
*
- * @param {Card[]} deck
+ * @param {[Card, Card]} deck
*
- * @returns {Card[]} new deck with reordered cards
+ * @returns {[Card, Card]} new deck with the 2 cards swapped
*/
-export function swapTopTwoCards(deck) {
+export function swapTwoCards(deck) {
throw new Error('Implement the swapTopTwoCards function');
}
/**
- * Put the top card of the given deck into a separate discard pile
+ * Rotate (shift) the position of the three cards (by one place)
*
- * @param {Card[]} deck
+ * @param {[Card, Card, Card]} deck
*
- * @returns {[Card, Card[]]} the top card of the given
- * deck and a new deck containing all the other cards
+ * @returns {[Card, Card, Card]} new deck with the 3 cards shifted by one position
*/
-export function discardTopCard(deck) {
+export function shiftThreeCardsAround(deck) {
throw new Error('Implement the discardTopCard function');
}
-/** @type Card[] **/
-const FACE_CARDS = ['jack', 'queen', 'king'];
-
/**
- * Insert face cards into the given deck
+ * Grab the chosen pile from the available piles
*
- * @param {Card[]} deck
+ * @param {{ chosen: Card[], disregarded: Card[] }} piles
+ *
+ * @returns {Card[]} the pile named chosen
+ */
+export function pickNamedPile(piles) {
+ // 🚨 Do NOT use piles.chosen or piles.disregarded.
+ throw new Error('Implement the pickNamedPile function');
+}
+
+/**
+ * Swap the chosen pile for the disregarded pile and the disregarded pile for the chosen pile
*
- * @returns {Card[]} new deck where the second,
- * third, and fourth cards are the face cards
+ * @param {{ chosen: Card[], disregarded: Card[] }} piles
+ * @returns {{ chosen: Card[], disregarded: Card[] }} new piles where the two piles are swapped
*/
-export function insertFaceCards(deck) {
- throw new Error('Implement the insertFaceCards function');
+export function swapNamedPile(piles) {
+ // 🪄 Don't break the magic.
+ // 🚨 Do NOT use piles.chosen or piles.disregarded.
+ // 🚨 Do NOT touch the next line or Elyse will accidentally reveal the trick.
+ return { chosen, disregarded };
}
diff --git a/exercises/concept/elyses-destructured-enchantments/enchantments.spec.js b/exercises/concept/elyses-destructured-enchantments/enchantments.spec.js
index 7f61a11cb8..c4d15a9ae3 100644
--- a/exercises/concept/elyses-destructured-enchantments/enchantments.spec.js
+++ b/exercises/concept/elyses-destructured-enchantments/enchantments.spec.js
@@ -1,87 +1,162 @@
-// @ts-check
-
+import { describe, expect, test } from '@jest/globals';
import {
- discardTopCard,
getFirstCard,
getSecondCard,
- insertFaceCards,
- swapTopTwoCards,
+ swapTwoCards,
+ shiftThreeCardsAround,
+ pickNamedPile,
+ swapNamedPile,
} from './enchantments';
+const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom');
+const customLogSymbol = Symbol.for('exercism.javascript.util.log');
+
+// Follow the instructions in case you are stuck on "list.method is not a function"
+class LimitedDeck {
+ constructor(values) {
+ this.values = values;
+ }
+
+ // Enables rest syntax and spread operator, as wel as for of, etc.
+ [Symbol.iterator]() {
+ return this.values[Symbol.iterator]();
+ }
+
+ // Log value in non-upgraded environments
+ toString() {
+ return this.values.toString();
+ }
+
+ // Overrides logging in node (ie. students working locally)
+ [customInspectSymbol](depth, inspectOptions, inspect) {
+ const inner = this.values[customInspectSymbol]
+ ? this.values[customInspectSymbol](depth, inspectOptions, inspect)
+ : this.values.toString();
+
+ return `List of (${inner})`;
+ }
+
+ // Overrides log overrides in web environment (ie. students working in editor)
+ [customLogSymbol](depth, inspectOptions, inspect) {
+ const inner = this.values[customLogSymbol]
+ ? this.values[customLogSymbol](depth, inspectOptions, inspect)
+ : this.values.toString();
+
+ return `List of (${inner})`;
+ }
+}
+
+function deck(...values) {
+ return new LimitedDeck(values);
+}
+
describe('getFirstCard', () => {
test('from a deck with a single card', () => {
- expect(getFirstCard([3])).toBe(3);
+ expect(getFirstCard(deck(3))).toBe(3);
});
test('from a deck with many cards', () => {
- expect(getFirstCard([8, 3, 9, 5])).toBe(8);
+ expect(getFirstCard(deck(8, 3, 9, 5))).toBe(8);
});
test('from an empty deck', () => {
- expect(getFirstCard([])).toBe(undefined);
+ expect(getFirstCard(deck())).toBe(undefined);
});
});
describe('getSecondCard', () => {
test('from a deck with two cards', () => {
- expect(getSecondCard([10, 4])).toBe(4);
+ expect(getSecondCard(deck(10, 4))).toBe(4);
});
test('from a deck with many cards', () => {
- expect(getSecondCard([2, 5, 1, 6])).toBe(5);
+ expect(getSecondCard(deck(2, 5, 7, 6))).toBe(5);
});
test('from an empty deck', () => {
- expect(getSecondCard([])).toBe(undefined);
+ expect(getSecondCard(deck())).toBe(undefined);
});
test('from a deck with one card', () => {
- expect(getSecondCard([8])).toBe(undefined);
+ expect(getSecondCard(deck(8))).toBe(undefined);
});
});
-describe('swapTopTwoCards', () => {
- test('in a deck with two cards', () => {
- expect(swapTopTwoCards([3, 6])).toStrictEqual([6, 3]);
+describe('swapTwoCards', () => {
+ test('swapping two numbered cards', () => {
+ expect(swapTwoCards(deck(3, 6))).toStrictEqual([6, 3]);
});
- test('in a deck with many cards', () => {
- expect(swapTopTwoCards([10, 4, 3, 7, 8])).toStrictEqual([4, 10, 3, 7, 8]);
+ test('swapping a high card with a low card', () => {
+ expect(swapTwoCards(deck(10, 2))).toStrictEqual([2, 10]);
});
-});
-describe('discardTopCard', () => {
- test('from a deck with one card', () => {
- expect(discardTopCard([7])).toStrictEqual([7, []]);
+ test('swapping a face card with a low card', () => {
+ expect(swapTwoCards(deck('king', 3))).toStrictEqual([3, 'king']);
});
+});
- test('from a deck with many cards', () => {
- expect(discardTopCard([9, 2, 10, 4])).toStrictEqual([9, [2, 10, 4]]);
+describe('shiftThreeCardsAround', () => {
+ test('consecutive numbers', () => {
+ expect(shiftThreeCardsAround(deck(6, 4, 5))).toStrictEqual([4, 5, 6]);
});
-});
-describe('insertFaceCards', () => {
- test('into a deck with many cards', () => {
- expect(insertFaceCards([3, 10, 7])).toStrictEqual([
- 3,
- 'jack',
- 'queen',
+ test('drop the face card to the bottom', () => {
+ expect(shiftThreeCardsAround(deck('king', 5, 2))).toStrictEqual([
+ 5,
+ 2,
'king',
- 10,
- 7,
]);
});
+});
- test('into a deck with one card', () => {
- expect(insertFaceCards([9])).toStrictEqual([9, 'jack', 'queen', 'king']);
+describe('pickNamedPile', () => {
+ test('keeps the chosen pile', () => {
+ const chosen = deck(3, 'jack', 'queen', 'king', 10, 7);
+ const disregarded = deck(4, 5, 6, 8, 9);
+ const piles = { chosen, disregarded };
+
+ expect(pickNamedPile(piles)).toStrictEqual(chosen);
});
- test('into a deck with no cards', () => {
- expect(insertFaceCards([])).toStrictEqual([
- undefined,
- 'jack',
- 'queen',
- 'king',
- ]);
+ test('returns the actual pile without recreating it', () => {
+ const chosen = deck(3, 'jack', 'queen', 'king', 10, 7);
+ const disregarded = deck(4, 5, 6, 8, 9);
+ const piles = { chosen, disregarded };
+
+ const result = pickNamedPile(piles);
+
+ chosen.values.push('joker');
+
+ expect(result).toStrictEqual(chosen);
+ });
+});
+
+describe('swapNamedPile', () => {
+ test('renames the piles', () => {
+ const face_pile = deck(3, 'jack', 'queen', 'king', 10, 7);
+ const numbers_pile = deck(4, 5, 6, 8, 9);
+ const piles = { chosen: numbers_pile, disregarded: face_pile };
+
+ expect(swapNamedPile(piles)).toStrictEqual({
+ chosen: face_pile,
+ disregarded: numbers_pile,
+ });
+ });
+
+ test('returns the actual piles without recreating them', () => {
+ const face_pile = deck(3, 'jack', 'queen', 'king', 10, 7);
+ const numbers_pile = deck(4, 5, 6, 8, 9);
+ const piles = { chosen: numbers_pile, disregarded: face_pile };
+
+ const result = swapNamedPile(piles);
+
+ face_pile.values.push('joker');
+ numbers_pile.values.push(2);
+
+ expect(result).toStrictEqual({
+ chosen: face_pile,
+ disregarded: numbers_pile,
+ });
});
});
diff --git a/exercises/concept/elyses-destructured-enchantments/eslint.config.mjs b/exercises/concept/elyses-destructured-enchantments/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/elyses-destructured-enchantments/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/elyses-destructured-enchantments/jest.config.js b/exercises/concept/elyses-destructured-enchantments/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/elyses-destructured-enchantments/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/elyses-destructured-enchantments/package.json b/exercises/concept/elyses-destructured-enchantments/package.json
index b53a69491b..d0bb86a683 100644
--- a/exercises/concept/elyses-destructured-enchantments/package.json
+++ b/exercises/concept/elyses-destructured-enchantments/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/concept/elyses-destructured-enchantments"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/concept/elyses-enchantments/.docs/instructions.md b/exercises/concept/elyses-enchantments/.docs/instructions.md
index f44e9fe7ab..8298853c08 100644
--- a/exercises/concept/elyses-enchantments/.docs/instructions.md
+++ b/exercises/concept/elyses-enchantments/.docs/instructions.md
@@ -9,6 +9,14 @@ of a certain card corresponds to the index in the array. That means
position 0 refers to the first card, position 1 to the second card
etc.
+
+~~~~exercism/note
+All but two functions should update the array of cards and then return the modified array - a common way of working known as the Builder pattern, which allows you to nicely daisy-chain functions together.
+
+The two exceptions are `getItem`, which should return the card at the given position, and `checkSizeOfStack` which should return `true` if the given size matches.
+~~~~
+
+
## 1. Retrieve a card from a stack
To pick a card, return the card at index `position` from
diff --git a/exercises/concept/elyses-enchantments/.docs/introduction.md b/exercises/concept/elyses-enchantments/.docs/introduction.md
index c07df79ac0..6f9c6a144f 100644
--- a/exercises/concept/elyses-enchantments/.docs/introduction.md
+++ b/exercises/concept/elyses-enchantments/.docs/introduction.md
@@ -33,12 +33,13 @@ numbers;
## Methods
-Some of the [methods][array_methods] that are available on every Array object can be used to add or remove from the array.
+Some of the [methods][mdn-array] that are available on every Array object can be used to add or remove from the array.
Here are a few to consider when working on this exercise:
### push
-> The `push()` method adds one or more elements to the end of an array and returns the new length of the array.[^1]
+A `value` can be _added_ to the end of an array by using `.push(value)`.
+The method returns the new length of the array.
```javascript
const numbers = [1, 'two', 3, 'four'];
@@ -49,8 +50,9 @@ numbers;
### pop
-> The `pop()` method removes the last element from an array and returns that element.
-> This method changes the length of the array.[^2]
+The _last_ `value` can be _removed_ from an array by using `.pop()`
+The method returns the removed value.
+The length of the array will be decreased because of this change.
```javascript
const numbers = [1, 'two', 3, 'four'];
@@ -61,8 +63,9 @@ numbers;
### shift
-> The `shift()` method removes the first element from an array and returns that removed element.
-> This method changes the length of the array.[^3]
+The _first_ `value` can be _removed_ from an array by using `.shift()`
+The method returns the removed value.
+The length of the array will be decreased because of this change.
```javascript
const numbers = [1, 'two', 3, 'four'];
@@ -73,7 +76,8 @@ numbers;
### unshift
-> The unshift() method adds one or more elements to the beginning of an array and returns the new length of the array.[^4]
+A `value` can be _added_ to the beginning of an array by using `.unshift(value)`.
+The method returns the new length of the array.
```javascript
const numbers = [1, 'two', 3, 'four'];
@@ -84,8 +88,8 @@ numbers;
### splice
-> The splice() method changes the contents of an array by removing or replacing existing elements and/or adding new elements in place.
-> This method returns an array containing the deleted elements.[^5]
+A `value` at a specific `index` can be _removed_ from an array by using `.splice(index, 1)`.
+The method returns the removed element(s).
```javascript
const numbers = [1, 'two', 3, 'four'];
@@ -94,12 +98,17 @@ numbers;
// => [1, 'two', 'one', 'four']
```
----
+
+~~~exercism/advanced
+These methods are more powerful than described:
-[^1]: `push`, MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/push (referenced September 29, 2021)
-[^2]: `pop`, MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/pop (referenced September 29, 2021)
-[^3]: `shift`, MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/shift (referenced September 29, 2021)
-[^4]: `unshift`, MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/unshift (referenced September 29, 2021)
-[^5]: `splice`, MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice (referenced September 29, 2021)
+- Both `push` and `unshift` allow you to push or unshift multiple values at once, by adding more arguments.
+ That is not necessary to complete this exercise.
+- Splice can remove multiple values by increasing the second argument.
+ That is not necessary to complete this exercise.
+- Splice can also add multiple values by adding them as arguments after the `deleteCount`.
+ This can be used to replace values, or insert values in the middle of an array (for example by removing 0 elements).
+ That is not necessary to complete this exercise.
+~~~
-[array_methods]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array
+[mdn-array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array
diff --git a/exercises/concept/elyses-enchantments/.eslintrc b/exercises/concept/elyses-enchantments/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/concept/elyses-enchantments/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/concept/elyses-enchantments/.gitignore b/exercises/concept/elyses-enchantments/.gitignore
index bdb912f98a..0c88ff6ec3 100644
--- a/exercises/concept/elyses-enchantments/.gitignore
+++ b/exercises/concept/elyses-enchantments/.gitignore
@@ -1,3 +1,5 @@
-node_modules
-yarn-error.log
-
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/elyses-enchantments/babel.config.js b/exercises/concept/elyses-enchantments/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/concept/elyses-enchantments/babel.config.js
+++ b/exercises/concept/elyses-enchantments/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/concept/elyses-enchantments/enchantments.spec.js b/exercises/concept/elyses-enchantments/enchantments.spec.js
index 9bf6100d83..15cdf6e5b7 100644
--- a/exercises/concept/elyses-enchantments/enchantments.spec.js
+++ b/exercises/concept/elyses-enchantments/enchantments.spec.js
@@ -1,14 +1,13 @@
-//@ts-check
-
+import { describe, expect, test } from '@jest/globals';
import {
+ checkSizeOfStack,
getItem,
- setItem,
- insertItemAtTop,
insertItemAtBottom,
+ insertItemAtTop,
removeItem,
- removeItemFromTop,
removeItemAtBottom,
- checkSizeOfStack,
+ removeItemFromTop,
+ setItem,
} from './enchantments';
describe('pick a card', () => {
@@ -115,11 +114,8 @@ describe('make cards disappear', () => {
const expected = [2, 3, 4];
if (stack[0] === undefined) {
- // eslint-disable-next-line no-undef
- fail(
- new Error(
- 'The card has disappeared, but the stack has not changed in size. This magic trick has turned into actual magic. Perhaps a different method of removing the card will result in a stack that Elyse can work with...'
- )
+ throw new Error(
+ 'The card has disappeared, but the stack has not changed in size. This magic trick has turned into actual magic. Perhaps a different method of removing the card will result in a stack that Elyse can work with...',
);
}
@@ -246,42 +242,34 @@ describe('make the bottom card disappear', () => {
});
describe('check your work', () => {
- describe('an empty stack of cards', () => {
- test('has 0 cards', () => {
- const stack = [];
+ test('an empty stack of cards has 0 cards', () => {
+ const stack = [];
- expect(checkSizeOfStack(stack, 0)).toBe(true);
- expect(checkSizeOfStack(stack, 1)).toBe(false);
- });
+ expect(checkSizeOfStack(stack, 0)).toBe(true);
+ expect(checkSizeOfStack(stack, 1)).toBe(false);
});
- describe('a stack with a single card', () => {
- test('has exactly 1 card', () => {
- const stack = [7];
+ test('a stack with a single card has exactly 1 card', () => {
+ const stack = [7];
- expect(checkSizeOfStack(stack, 0)).toBe(false);
- expect(checkSizeOfStack(stack, 1)).toBe(true);
- expect(checkSizeOfStack(stack, 2)).toBe(false);
- });
+ expect(checkSizeOfStack(stack, 0)).toBe(false);
+ expect(checkSizeOfStack(stack, 1)).toBe(true);
+ expect(checkSizeOfStack(stack, 2)).toBe(false);
});
- describe('a stack with the even cards', () => {
- test('has exactly 4 cards', () => {
- const stack = [2, 4, 6, 8];
+ test('a stack with the even cards has exactly 4 cards', () => {
+ const stack = [2, 4, 6, 8];
- expect(checkSizeOfStack(stack, 3)).toBe(false);
- expect(checkSizeOfStack(stack, 4)).toBe(true);
- expect(checkSizeOfStack(stack, 5)).toBe(false);
- });
+ expect(checkSizeOfStack(stack, 3)).toBe(false);
+ expect(checkSizeOfStack(stack, 4)).toBe(true);
+ expect(checkSizeOfStack(stack, 5)).toBe(false);
});
- describe('a stack with the odd cards', () => {
- test('has exactly 5 cards', () => {
- const stack = [1, 3, 5, 7, 9];
+ test('a stack with the odd cards has exactly 5 cards', () => {
+ const stack = [1, 3, 5, 7, 9];
- expect(checkSizeOfStack(stack, 3)).toBe(false);
- expect(checkSizeOfStack(stack, 4)).toBe(false);
- expect(checkSizeOfStack(stack, 5)).toBe(true);
- });
+ expect(checkSizeOfStack(stack, 3)).toBe(false);
+ expect(checkSizeOfStack(stack, 4)).toBe(false);
+ expect(checkSizeOfStack(stack, 5)).toBe(true);
});
});
diff --git a/exercises/concept/elyses-enchantments/eslint.config.mjs b/exercises/concept/elyses-enchantments/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/elyses-enchantments/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/elyses-enchantments/jest.config.js b/exercises/concept/elyses-enchantments/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/elyses-enchantments/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/elyses-enchantments/package.json b/exercises/concept/elyses-enchantments/package.json
index 9a121985b7..b3cb93a328 100644
--- a/exercises/concept/elyses-enchantments/package.json
+++ b/exercises/concept/elyses-enchantments/package.json
@@ -13,20 +13,25 @@
"directory": "exercises/concept/elyses-enchantments"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/concept/elyses-looping-enchantments/.docs/instructions.md b/exercises/concept/elyses-looping-enchantments/.docs/instructions.md
index 9111eb7b59..23979e0fa3 100644
--- a/exercises/concept/elyses-looping-enchantments/.docs/instructions.md
+++ b/exercises/concept/elyses-looping-enchantments/.docs/instructions.md
@@ -8,7 +8,11 @@ To keep things simple, she only uses cards with values 1-10.
Elyse wants to know how many cards of a particular type she has in her deck.
Write a function `cardTypeCheck` that takes two parameters: an array of cards (Elyse's deck) and the type of card to count.
+
+
+~~~exercism/note
The function should use `forEach` and return the number of cards in the deck of the specified type.
+~~~
```javascript
const cardType = 3;
@@ -23,7 +27,11 @@ For another trick, Elyse needs to know how many odd or even cards there are in h
Implement a function `determineOddEvenCards` that takes in two parameters: an array of cards (Elyse's deck), and a boolean (true is analogous to 'even', and false is analogous to 'odd').
This function should return a single number: the number of odd or even cards there are (depending on the value of the second argument) in the deck.
+
+
+~~~exercism/note
To practice, use a `for...of` loop in the function implementation this time.
+~~~
```javascript
determineOddEvenCards([1, 2, 3, 1, 5, 6], true);
diff --git a/exercises/concept/elyses-looping-enchantments/.eslintrc b/exercises/concept/elyses-looping-enchantments/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/concept/elyses-looping-enchantments/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/concept/elyses-looping-enchantments/.gitignore b/exercises/concept/elyses-looping-enchantments/.gitignore
new file mode 100644
index 0000000000..0c88ff6ec3
--- /dev/null
+++ b/exercises/concept/elyses-looping-enchantments/.gitignore
@@ -0,0 +1,5 @@
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/elyses-looping-enchantments/babel.config.js b/exercises/concept/elyses-looping-enchantments/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/concept/elyses-looping-enchantments/babel.config.js
+++ b/exercises/concept/elyses-looping-enchantments/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/concept/elyses-looping-enchantments/enchantments.js b/exercises/concept/elyses-looping-enchantments/enchantments.js
index 6f9ae7453e..4de26a6047 100644
--- a/exercises/concept/elyses-looping-enchantments/enchantments.js
+++ b/exercises/concept/elyses-looping-enchantments/enchantments.js
@@ -9,6 +9,7 @@
* @returns {number} number of cards of a single type there are in the deck
*/
export function cardTypeCheck(stack, card) {
+ // 🚨 Use .forEach
throw new Error('Implement the cardTypeCheck function');
}
@@ -20,5 +21,6 @@ export function cardTypeCheck(stack, card) {
* @returns {number} number of cards that are either odd or even (depending on `type`)
*/
export function determineOddEvenCards(stack, type) {
+ // 🚨 Use a `for...of` loop
throw new Error('Implement the determineOddEvenCards function');
}
diff --git a/exercises/concept/elyses-looping-enchantments/enchantments.spec.js b/exercises/concept/elyses-looping-enchantments/enchantments.spec.js
index ff829883fc..3009f52f79 100644
--- a/exercises/concept/elyses-looping-enchantments/enchantments.spec.js
+++ b/exercises/concept/elyses-looping-enchantments/enchantments.spec.js
@@ -1,3 +1,4 @@
+import { describe, expect, test } from '@jest/globals';
import { cardTypeCheck, determineOddEvenCards } from './enchantments';
const TYPE_IS_ODD = false;
diff --git a/exercises/concept/elyses-looping-enchantments/eslint.config.mjs b/exercises/concept/elyses-looping-enchantments/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/elyses-looping-enchantments/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/elyses-looping-enchantments/jest.config.js b/exercises/concept/elyses-looping-enchantments/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/elyses-looping-enchantments/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/elyses-looping-enchantments/package.json b/exercises/concept/elyses-looping-enchantments/package.json
index b35eb23e85..6dab8650b0 100644
--- a/exercises/concept/elyses-looping-enchantments/package.json
+++ b/exercises/concept/elyses-looping-enchantments/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/concept/elyses-looping-enchantments"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/concept/elyses-transformative-enchantments/.docs/introduction.md b/exercises/concept/elyses-transformative-enchantments/.docs/introduction.md
index 9744d2d5e2..92ed35f233 100644
--- a/exercises/concept/elyses-transformative-enchantments/.docs/introduction.md
+++ b/exercises/concept/elyses-transformative-enchantments/.docs/introduction.md
@@ -61,7 +61,7 @@ arr.reduce(
return accumulator;
},
- { even: [], odd: [] }
+ { even: [], odd: [] },
);
// => { even: [2, 4], odd: [1, 3] }
```
diff --git a/exercises/concept/elyses-transformative-enchantments/.eslintrc b/exercises/concept/elyses-transformative-enchantments/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/concept/elyses-transformative-enchantments/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/concept/elyses-transformative-enchantments/.gitignore b/exercises/concept/elyses-transformative-enchantments/.gitignore
index bdb912f98a..0c88ff6ec3 100644
--- a/exercises/concept/elyses-transformative-enchantments/.gitignore
+++ b/exercises/concept/elyses-transformative-enchantments/.gitignore
@@ -1,3 +1,5 @@
-node_modules
-yarn-error.log
-
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/elyses-transformative-enchantments/babel.config.js b/exercises/concept/elyses-transformative-enchantments/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/concept/elyses-transformative-enchantments/babel.config.js
+++ b/exercises/concept/elyses-transformative-enchantments/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/concept/elyses-transformative-enchantments/enchantments.spec.js b/exercises/concept/elyses-transformative-enchantments/enchantments.spec.js
index 320ad57573..0b10805fa7 100644
--- a/exercises/concept/elyses-transformative-enchantments/enchantments.spec.js
+++ b/exercises/concept/elyses-transformative-enchantments/enchantments.spec.js
@@ -1,13 +1,12 @@
-// @ts-check
-
+import { describe, expect, test } from '@jest/globals';
import {
- seeingDouble,
- threeOfEachThree,
middleTwo,
- sandwichTrick,
- twoIsSpecial,
perfectlyOrdered,
reorder,
+ sandwichTrick,
+ seeingDouble,
+ threeOfEachThree,
+ twoIsSpecial,
} from './enchantments';
describe('seeingDouble', () => {
diff --git a/exercises/concept/elyses-transformative-enchantments/eslint.config.mjs b/exercises/concept/elyses-transformative-enchantments/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/elyses-transformative-enchantments/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/elyses-transformative-enchantments/jest.config.js b/exercises/concept/elyses-transformative-enchantments/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/elyses-transformative-enchantments/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/elyses-transformative-enchantments/package.json b/exercises/concept/elyses-transformative-enchantments/package.json
index 6d0bdde9d7..f866b089f7 100644
--- a/exercises/concept/elyses-transformative-enchantments/package.json
+++ b/exercises/concept/elyses-transformative-enchantments/package.json
@@ -14,20 +14,25 @@
"directory": "exercises/concept/elyses-transformative-enchantments"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/concept/factory-sensors/.docs/instructions.md b/exercises/concept/factory-sensors/.docs/instructions.md
index b8c47f3f9b..ccee09cce3 100644
--- a/exercises/concept/factory-sensors/.docs/instructions.md
+++ b/exercises/concept/factory-sensors/.docs/instructions.md
@@ -1,6 +1,8 @@
# Instructions
-Elena is the new quality manager of a newspaper factory. As she has just arrived in the company, she has decided to review some of the processes in the factory to see what could be improved. She found out that technicians are doing a lot of quality checks by hand. She sees there is a good opportunity for automation and asks you, a freelance developer, to develop a piece of software to monitor some of the machines.
+Elena is the new quality manager of a newspaper factory. As she has just arrived in the company, she has decided to review some of the processes in the factory to see what could be improved.
+She found out that technicians are doing a lot of quality checks by hand.
+She sees there is a good opportunity for automation and asks you, a freelance developer, to develop a piece of software to monitor some of the machines.
## 1. Monitor the humidity level of the room
@@ -59,24 +61,30 @@ Now that your machine can detect errors, you will implement a function that reac
- If the temperature is too high, you will either shut down the machine if the temperature exceeds 600°C or turn on a warning light if it is less than that.
- If another error happens, you'll rethrow it.
-Implements a function `monitorTheMachine` that takes an argument `actions`.
+Implement a function `monitorTheMachine` that takes an argument `actions`.
`actions` is an object that has 4 properties :
-- `check` is a _*function*_ that, when called, checks the temperature of the machine.
- It may throw various errors
-
-- `alertDeadSensor` is a _*function*_ that, when called, alerts a technician that the temperature's sensor is dead.
-
-- `alertOverheating` is a _*function*_ that, when called, will turn on a warning light on the machine.
-
-- `shutdown` is a _*function*_ that, when called, will turn off the machine.
-
-The `monitorTheMachine` function should call `check()`. If it passes, the function should not return anything. However, it may `throw` an error. When this happens, you should, depending on the error:
-
-- `ArgumentError`: when this happens, call the `alertDeadSensor` function.
-- `OverheatingError`: when this happens, if the temperature is less than 600 °C, call the `alertOverheating` function to turn on the warning light. If the temperature exceeds 600°C, the situation is critical, call the `shutdown` function.
-- _anything else_: when this happens, rethrow the error
+| property | description |
+| ------------------ | ----------------------------------------------------------------------------------- |
+| `check` | a _*function*_. Checks the temperature of the machine. It may throw various errors. |
+| `alertDeadSensor` | a _*function*_. Alerts a technician that the temperature's sensor is dead. |
+| `alertOverheating` | a _*function*_. Will turn on a warning light on the machine. |
+| `shutdown` | a _*function*_. Will turn off the machine. |
+
+The `monitorTheMachine(actions)` function should internally call `check()`.
+If that passes, the function should not return anything.
+If that `throw`s an error, different behaviour is expected:
+
+| exception | expected behaviour |
+| ------------------ | ------------------------------------ |
+| `ArgumentError` | call the `alertDeadSensor` function. |
+| `OverheatingError` | execute the overheating protocol. |
+| _(anything else)_ | rethrow the error |
+
+**Overheating protocol**
+If the temperature is less than 600 °C, turn on the warning light by calling the `alertOverheating` function.
+If the temperature exceeds 600 °C, the situation is critical, so call the `shutdown` function.
```javascript
monitorTheMachine({
diff --git a/exercises/concept/factory-sensors/.eslintrc b/exercises/concept/factory-sensors/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/concept/factory-sensors/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/concept/factory-sensors/.gitignore b/exercises/concept/factory-sensors/.gitignore
index bdb912f98a..0c88ff6ec3 100644
--- a/exercises/concept/factory-sensors/.gitignore
+++ b/exercises/concept/factory-sensors/.gitignore
@@ -1,3 +1,5 @@
-node_modules
-yarn-error.log
-
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/factory-sensors/.meta/config.json b/exercises/concept/factory-sensors/.meta/config.json
index 71e498c5a9..50e42d1b4c 100644
--- a/exercises/concept/factory-sensors/.meta/config.json
+++ b/exercises/concept/factory-sensors/.meta/config.json
@@ -4,7 +4,9 @@
],
"contributors": [
"SleeplessByte",
- "junedev"
+ "junedev",
+ "themetar",
+ "orimdominic"
],
"files": {
"solution": [
diff --git a/exercises/concept/factory-sensors/babel.config.js b/exercises/concept/factory-sensors/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/concept/factory-sensors/babel.config.js
+++ b/exercises/concept/factory-sensors/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/concept/factory-sensors/eslint.config.mjs b/exercises/concept/factory-sensors/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/factory-sensors/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/factory-sensors/factory-sensors.js b/exercises/concept/factory-sensors/factory-sensors.js
index cfc2b02d67..be71cfd434 100644
--- a/exercises/concept/factory-sensors/factory-sensors.js
+++ b/exercises/concept/factory-sensors/factory-sensors.js
@@ -16,7 +16,7 @@ export class OverheatingError extends Error {
* @throws {Error}
*/
export function checkHumidityLevel(humidityPercentage) {
- throw new Error('Implement the checkHumidity function');
+ throw new Error('Remove this line and implement the function');
}
/**
@@ -26,7 +26,7 @@ export function checkHumidityLevel(humidityPercentage) {
* @throws {ArgumentError|OverheatingError}
*/
export function reportOverheating(temperature) {
- throw new Error('Implement the reportOverheating function');
+ throw new Error('Remove this line and implement the function');
}
/**
@@ -41,5 +41,5 @@ export function reportOverheating(temperature) {
* @throws {ArgumentError|OverheatingError|Error}
*/
export function monitorTheMachine(actions) {
- throw new Error('Implement the monitorTheMachine function');
+ throw new Error('Remove this line and implement the function');
}
diff --git a/exercises/concept/factory-sensors/factory-sensors.spec.js b/exercises/concept/factory-sensors/factory-sensors.spec.js
index 3eda0876f5..5ecbd96202 100644
--- a/exercises/concept/factory-sensors/factory-sensors.spec.js
+++ b/exercises/concept/factory-sensors/factory-sensors.spec.js
@@ -1,16 +1,25 @@
+import { beforeEach, describe, expect, test, jest } from '@jest/globals';
import {
+ ArgumentError,
checkHumidityLevel,
- reportOverheating,
monitorTheMachine,
- ArgumentError,
OverheatingError,
+ reportOverheating,
} from './factory-sensors';
describe('checkHumidityLevel', () => {
+ test('should throw if the humidity percentage is 71', () => {
+ expect(() => checkHumidityLevel(71)).toThrow();
+ });
+
test('should throw if the humidity percentage is 100', () => {
expect(() => checkHumidityLevel(100)).toThrow();
});
+ test('should not throw if the humidity level is 70', () => {
+ expect(() => checkHumidityLevel(70)).not.toThrow();
+ });
+
test('should not throw if the humidity level is 53', () => {
expect(() => checkHumidityLevel(53)).not.toThrow();
});
@@ -25,6 +34,10 @@ describe('reportOverheating', () => {
expect(() => reportOverheating(null)).toThrow(ArgumentError);
});
+ test('should not throw if the temperature is 0°C', () => {
+ expect(() => reportOverheating(0)).not.toThrow();
+ });
+
test('should throw an OverheatingError if the temperature is 501°C', () => {
expect(() => reportOverheating(501)).toThrow(OverheatingError);
@@ -77,7 +90,7 @@ describe('monitorTheMachine', () => {
expect(actions.shutdown).not.toHaveBeenCalled();
});
- test('should call only the shutdown action if the check throws an OverheatingError with a temperature equals to 651°C', () => {
+ test('should call only the shutdown action if the check throws an OverheatingError with a temperature of 651°C', () => {
actions.check = jest.fn(() => {
throw new OverheatingError(651);
});
diff --git a/exercises/concept/factory-sensors/jest.config.js b/exercises/concept/factory-sensors/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/factory-sensors/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/factory-sensors/package.json b/exercises/concept/factory-sensors/package.json
index 9b8e1d53a1..b7915d0967 100644
--- a/exercises/concept/factory-sensors/package.json
+++ b/exercises/concept/factory-sensors/package.json
@@ -9,20 +9,25 @@
"directory": "exercises/concept/factory-sensors"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/concept/freelancer-rates/.docs/instructions.md b/exercises/concept/freelancer-rates/.docs/instructions.md
index 790980e020..85649c1e77 100644
--- a/exercises/concept/freelancer-rates/.docs/instructions.md
+++ b/exercises/concept/freelancer-rates/.docs/instructions.md
@@ -1,13 +1,19 @@
# Instructions
-In this exercise you will be writing code to help a freelancer communicate with his clients about the prices of certain projects. You will write a few utility functions to quickly calculate the costs for the clients.
+In this exercise you will be writing code to help a freelancer communicate with their clients about the prices of certain projects.
+You will write a few utility functions to quickly calculate the costs for the clients.
+
+```exercism/note
+The `ratePerHour` variable and the `dayRate` function are related to money.
+The units of measurement are money for a unit of time: hours and days respectively.
+```
## 1. Calculate the day rate given an hourly rate
-A client contacts the freelancer to enquire about his rates.
-The freelancer explains that he **_works 8 hours a day._**
-However, the freelancer knows only his hourly rates for the project.
-Help him estimate a day rate given an hourly rate.
+A client contacts the freelancer to enquire about their rates.
+The freelancer explains that they **_work 8 hours a day._**
+However, the freelancer knows only their hourly rates for the project.
+Help them estimate a day rate given an hourly rate.
```javascript
dayRate(89);
@@ -19,7 +25,7 @@ The day rate does not need to be rounded or changed to a "fixed" precision.
## 2. Calculate the number of workdays given a fixed budget
Another day, a project manager offers the freelancer to work on a project with a fixed budget.
-Given the fixed budget and the freelancer's hourly rate, help him calculate the number of days he would work until the budget is exhausted.
+Given the fixed budget and the freelancer's hourly rate, help them calculate the number of days they would work until the budget is exhausted.
The result _must_ be **rounded down** to the nearest whole number.
```javascript
@@ -29,10 +35,10 @@ daysInBudget(20000, 89);
## 3. Calculate the discounted rate for large projects
-Often, the freelancer's clients hire him for projects spanning over multiple months.
+Often, the freelancer's clients hire them for projects spanning over multiple months.
In these cases, the freelancer decides to offer a discount for every full month, and the remaining days are billed at day rate.
-**_Every month has 22 billable days._**
-Help him estimate his cost for such projects, given an hourly rate, the number of days the project spans, and a monthly discount rate.
+Your excellent work-life balance means that you only work 22 days in each calendar month, so **_every month has 22 billable days._**
+Help them estimate their cost for such projects, given an hourly rate, the number of billable days the project contains, and a monthly discount rate.
The discount is always passed as a number, where `42%` becomes `0.42`. The result _must_ be **rounded up** to the nearest whole number.
```javascript
diff --git a/exercises/concept/freelancer-rates/.docs/introduction.md b/exercises/concept/freelancer-rates/.docs/introduction.md
index 3e2edcaedd..f68386e39a 100644
--- a/exercises/concept/freelancer-rates/.docs/introduction.md
+++ b/exercises/concept/freelancer-rates/.docs/introduction.md
@@ -14,7 +14,8 @@ Otherwise, the `number` type is likely the better option.
### Rounding
-There is a built-in global object called `Math` that provides various [rounding functions][ref-math-object-rounding]. For example, you can round down (`floor`) or round up (`ceil`) decimal numbers to the nearest whole numbers.
+There is a built-in global object called `Math` that provides various [rounding functions][ref-math-object-rounding].
+For example, you can round down (`floor`) or round up (`ceil`) decimal numbers to the nearest whole numbers.
```javascript
Math.floor(234.34); // => 234
diff --git a/exercises/concept/freelancer-rates/.eslintrc b/exercises/concept/freelancer-rates/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/concept/freelancer-rates/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/concept/freelancer-rates/.gitignore b/exercises/concept/freelancer-rates/.gitignore
index bdb912f98a..0c88ff6ec3 100644
--- a/exercises/concept/freelancer-rates/.gitignore
+++ b/exercises/concept/freelancer-rates/.gitignore
@@ -1,3 +1,5 @@
-node_modules
-yarn-error.log
-
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/freelancer-rates/.meta/exemplar.js b/exercises/concept/freelancer-rates/.meta/exemplar.js
index 2f2959787e..7b2f91fad9 100644
--- a/exercises/concept/freelancer-rates/.meta/exemplar.js
+++ b/exercises/concept/freelancer-rates/.meta/exemplar.js
@@ -49,11 +49,12 @@ export function daysInBudget(budget, ratePerHour) {
* @returns {number} the discounted rate, rounded up
*/
export function priceWithMonthlyDiscount(ratePerHour, numDays, discount) {
- const numMonths = Math.floor(numDays / 22);
- const monthlyRate = 22 * dayRate(ratePerHour);
+ const billableDaysPerMonth = 22;
+ const numMonths = Math.floor(numDays / billableDaysPerMonth);
+ const monthlyRate = billableDaysPerMonth * dayRate(ratePerHour);
const monthlyDiscountedRate = (1 - discount) * monthlyRate;
- const numExtraDays = numDays % 22;
+ const numExtraDays = numDays % billableDaysPerMonth;
const priceExtraDays = numExtraDays * dayRate(ratePerHour);
return Math.ceil(numMonths * monthlyDiscountedRate + priceExtraDays);
diff --git a/exercises/concept/freelancer-rates/babel.config.js b/exercises/concept/freelancer-rates/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/concept/freelancer-rates/babel.config.js
+++ b/exercises/concept/freelancer-rates/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/concept/freelancer-rates/eslint.config.mjs b/exercises/concept/freelancer-rates/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/freelancer-rates/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/freelancer-rates/freelancer-rates.spec.js b/exercises/concept/freelancer-rates/freelancer-rates.spec.js
index 046d8ac479..9c8a3d5e60 100644
--- a/exercises/concept/freelancer-rates/freelancer-rates.spec.js
+++ b/exercises/concept/freelancer-rates/freelancer-rates.spec.js
@@ -1,5 +1,4 @@
-// @ts-check
-
+import { describe, expect, test } from '@jest/globals';
import {
dayRate,
daysInBudget,
@@ -36,57 +35,50 @@ describe('day rate', () => {
});
describe('days in budget', () => {
- describe('with a budget of 1280', () => {
- test('at 16/hour', () => {
- const actual = daysInBudget(1280, 16);
- const expected = 10;
+ test('with a budget of 1280 at 16/hour', () => {
+ const actual = daysInBudget(1280, 16);
+ const expected = 10;
- expect(actual).toBeCloseTo(expected, DIFFERENCE_PRECISION_IN_DIGITS);
- });
+ expect(actual).toBeCloseTo(expected, DIFFERENCE_PRECISION_IN_DIGITS);
+ });
- test('at 25/hour', () => {
- const actual = daysInBudget(1280, 25);
- const expected = 6;
+ test('with a budget of 1280 at 25/hour', () => {
+ const actual = daysInBudget(1280, 25);
+ const expected = 6;
- expect(actual).toBeCloseTo(expected, DIFFERENCE_PRECISION_IN_DIGITS);
- });
+ expect(actual).toBeCloseTo(expected, DIFFERENCE_PRECISION_IN_DIGITS);
+ });
- describe('with a budget of 835', () => {
- test('at 12/hour', () => {
- const actual = daysInBudget(835, 12);
- const expected = 8;
+ test('with a budget of 835 at 12/hour', () => {
+ const actual = daysInBudget(835, 12);
+ const expected = 8;
- expect(actual).toBeCloseTo(expected, DIFFERENCE_PRECISION_IN_DIGITS);
- });
- });
+ expect(actual).toBeCloseTo(expected, DIFFERENCE_PRECISION_IN_DIGITS);
});
});
describe('cost with monthly discount', () => {
- describe('at 16/hour', () => {
- test('for 70 days', () => {
- const actual = priceWithMonthlyDiscount(16, 70, 0);
- const expected = 8960;
- expect(actual).toBeCloseTo(expected, DIFFERENCE_PRECISION_IN_DIGITS);
- });
-
- test('for 130 days with 15% discount', () => {
- const actual = priceWithMonthlyDiscount(16, 130, 0.15);
- const expected = 14528;
- expect(actual).toBeCloseTo(expected, DIFFERENCE_PRECISION_IN_DIGITS);
- });
+ test('at 16/hour for 70 days', () => {
+ const actual = priceWithMonthlyDiscount(16, 70, 0);
+ const expected = 8960;
+ expect(actual).toBeCloseTo(expected, DIFFERENCE_PRECISION_IN_DIGITS);
+ });
+
+ test('at 16/hour for 130 days with 15% discount', () => {
+ const actual = priceWithMonthlyDiscount(16, 130, 0.15);
+ const expected = 14528;
+ expect(actual).toBeCloseTo(expected, DIFFERENCE_PRECISION_IN_DIGITS);
+ });
+
+ test('at 29.654321/hour for 220 days with 11.2%', () => {
+ const actual = priceWithMonthlyDiscount(29.654321, 220, 0.112);
+ const expected = 46347;
+ expect(actual).toBeCloseTo(expected, DIFFERENCE_PRECISION_IN_DIGITS);
});
- describe('at 29.654321/hour', () => {
- test('for 220 days with 11.2%', () => {
- const actual = priceWithMonthlyDiscount(29.654321, 220, 0.112);
- const expected = 46347;
- expect(actual).toBeCloseTo(expected, DIFFERENCE_PRECISION_IN_DIGITS);
- });
-
- test('for 155 days with 25.47% discount', () => {
- const actual = priceWithMonthlyDiscount(29.654321, 155, 0.2547);
- const expected = 27467;
- expect(actual).toBeCloseTo(expected, DIFFERENCE_PRECISION_IN_DIGITS);
- });
+
+ test('at 29.654321/hour for 155 days with 25.47% discount', () => {
+ const actual = priceWithMonthlyDiscount(29.654321, 155, 0.2547);
+ const expected = 27467;
+ expect(actual).toBeCloseTo(expected, DIFFERENCE_PRECISION_IN_DIGITS);
});
});
diff --git a/exercises/concept/freelancer-rates/jest.config.js b/exercises/concept/freelancer-rates/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/freelancer-rates/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/freelancer-rates/package.json b/exercises/concept/freelancer-rates/package.json
index ebfd48ea05..f506e2f30a 100644
--- a/exercises/concept/freelancer-rates/package.json
+++ b/exercises/concept/freelancer-rates/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/concept/freelancer-rates"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/concept/fruit-picker/.docs/hints.md b/exercises/concept/fruit-picker/.docs/hints.md
index 60894553ac..bf33da9664 100644
--- a/exercises/concept/fruit-picker/.docs/hints.md
+++ b/exercises/concept/fruit-picker/.docs/hints.md
@@ -4,34 +4,28 @@
- Callback functions are functions passed as an argument to a calling function.
- Callback functions must meet the specification of the calling function.
-- These callbacks are all synchronous, meaning they all must return a value.
-## 1. Check if the grocer's service is online
+## 1. Notify with a successful message
-- Use the imported API function `checkStatus` in your function.
-- Pass a callback function to `checkStatus`. It should expect to receive a string argument.
-- Return the result from the `checkStatus` API function.
+- Use the imported API function `notify` in your function.
+- Pass an object to `notify`. It should have a property of `message`. Think about what the message value should be.
+- Pay attention to the JSDoc type hint -- the function should not return a value.
-## 2. See if the grocer has some fruit
+## 2. Notify with an error message
-- Use the imported API function `checkInventory` in your function.
-- Create the query _object_ to pass as an argument.
- - Follow the template in the instructions.
-- Pass along the callback function to the `checkInventory` API function.
-- Return the result from the `checkInventory` API function.
+- Use the imported API function `notify` in your function.
+- Pass another object to `notify`. It should have a property of `message`. The message value should differ from the last part.
+- Pay attention to the comments [JSDoc] above the function definition:
+ - The function should not return a value.
-## 3. Create a callback to buy fruit if the inventory is available
+## 3. Place an order to buy fruit
-- If the `err` argument is not null, throw a new error using `err`'s message.
- - See [Error()][mdn-error-constructor]
-- if the `err` is null, ignore it and just respond to the value of `isAvailable`
- - This pattern is sometimes called a [`Node.js`-style callback][node-js-callback].
-- Return the action determined by `isAvailable`
+- Use the already imported `order` function.
+- Pay attention to the JSDoc type hints -- the function should receive 3 arguments and not return a value.
+- Reuse the previously created callbacks as arguments to the order function.
-## 4. Put it all together
+## 4. Refactor your work into a more concise function
-- Reuse the functions you have already written, composing them together.
-- Return the value of the callback function.
-
-[mdn-error-constructor]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/Error
-[node-js-callback]: https://nodejs.org/en/knowledge/getting-started/control-flow/what-are-callbacks/
+- Reuse the functions from the previous tasks.
+- Pay attention to the JSDoc type hints -- the function should receive 2 arguments and not return a value.
+- Combine the arguments into an object representing the query to the grocer to place an order.
diff --git a/exercises/concept/fruit-picker/.docs/instructions.md b/exercises/concept/fruit-picker/.docs/instructions.md
index 9c41e710c7..278dd8c47f 100644
--- a/exercises/concept/fruit-picker/.docs/instructions.md
+++ b/exercises/concept/fruit-picker/.docs/instructions.md
@@ -1,28 +1,38 @@
# Instructions
-You are creating a new online portal for your patrons to order their fruit fresh from the grocer. The grocer has an API that you can use to see if they have the inventory desired by your customers. You need to create a small library of functions for interacting with the grocer's API.
+You are creating a new online portal for your patrons to order their fruit fresh from the grocer.
+The grocer has an API that you can use to see if they have the inventory desired by your customers.
+You need to create a small library of functions for interacting with the grocer's API.
-## 1. Create a callback to be called when the order is successful
+## 1. Notify your customer when their order was successful
-Write a callback function called `onSuccess` to be called when the order is successful. It should invoke the imported `notify` function passing a success message to it.
+The portal should notify your customer if their order was successful.
+Define the `onSuccess` callback function that will be called if the order was successful due to the grocer having enough fruit in stock.
+This function should invoke the imported `notify` function, passing `{ message: 'SUCCESS' }` to let your customer know that their order was successful.
```javascript
onSuccess();
// => `notify` called with `{ message: 'SUCCESS' }`
```
-## 2. Create a callback to be called when the order fails with an error
+## 2. Notify your customer when their order was unsuccessful
-Write a callback function called `onError` to be called when the order encounters an error. It should invoke the imported `notify` function passing an error message to it.
+The portal should notify your customer if their order was unsuccessful.
+Define the `onError` callback function that will be called if the order was unsuccessful because the grocer _does not have_ the fruit in stock or there was an error.
+This function should invoke the imported `notify` function, passing `{ message: 'ERROR' }` to let your customer know that their order was unsuccessful.
```javascript
onError();
// => `notify` called with `{ message: 'ERROR' }`
```
-## 3. Create a wrapper to wrap the external api function
+## 3. Create an API wrapper to wrap the grocer's API order function
-The grocer's API provides a function to order from their inventory called `order`. It receives three arguments: a _query_, a _callback_ function to be invoked when the order is successful, and a _callback_ function to be invoked when the order encounters an error. You decide to wrap the api function call in a newly defined function `orderFromGrocer` to insulate your codebase from external changes. Your function should forward the arguments (which match the provided api function) to the api function.
+Fruit orders are placed through the grocer's API via the provided `order` function.
+This function receives three arguments: a _query_, containing the `variety` and `quantity` of fruit requested, a _callback_ function to be invoked when the order is successful, and a _callback_ function to be invoked when the order encounters an error.
+
+You want to insulate your codebase from potential external changes and decide to wrap the call to the `order` function inside a new function named `orderFromGrocer`.
+Implement the `orderFromGrocer` function that attempts to place an order via a call to the grocer's API `order` function, making sure to forward the arguments passed into `orderFromGrocer` to the API call.
The query takes the form of an _object_:
@@ -37,16 +47,18 @@ const query = {
orderFromGrocer(
{ variety: 'pear', quantity: 12 },
exampleSuccessCallback,
- exampleErrorCallback
+ exampleErrorCallback,
);
// => `order` was called with the query and the callbacks
```
-## 4. Create a convenient short function
+## 4. Simplify handling placed orders
-You find that you are calling this function from many different places with the same functions. Seeing an opportunity to refactor your code, you want to create a function where you can supply the variety and quantity to order as arguments.
+Your customers are now able to place fruit orders via your portal, however, you notice that you are invoking the `orderFromGrocer` function in many different places across your codebase, each time having to pass in a `query` and the two `callback` functions as arguments.
+Seeing an opportunity to refactor your code, you think it would be simpler if you could place an order by just passing the `variety` and `quantity` of fruit required.
+Define the `postOrder` helper function that takes `variety` and `quantity` as arguments and attempts to place an order with the grocer.
```javascript
postOrder('peach', 100);
-// => order submitted for 100 peaches
+// => order placed for 100 peaches
```
diff --git a/exercises/concept/fruit-picker/.docs/introduction.md b/exercises/concept/fruit-picker/.docs/introduction.md
index 3a88419e35..4daee344f1 100644
--- a/exercises/concept/fruit-picker/.docs/introduction.md
+++ b/exercises/concept/fruit-picker/.docs/introduction.md
@@ -2,7 +2,9 @@
## Callback functions
-Callback functions are functions passed as arguments. This programming pattern creates a sequence of function calls in both synchronous and asynchronous programming. Writing a callback function is no different from writing a function; however, the callback function must match the signature defined by the calling function.
+Callback functions are functions passed as arguments.
+This programming pattern creates a sequence of function calls in both synchronous and asynchronous programming.
+Writing a callback function is no different from writing a function; however, the callback function must match the signature defined by the calling function.
```javascript
const squareLength = 5;
@@ -20,10 +22,18 @@ function areaOfSquare(number) {
applyToSquare(areaOfSquare); // => 25
```
-You may also write callbacks as a function expression:
+You may also write callbacks as a function expression, anonymous function expression, or arrow function expression:
```javascript
applyToSquare(function squarePerimeter(side) {
return side * 4;
});
+
+applyToSquare(function (side) {
+ return side * 4;
+});
+
+applyToSquare((side) => {
+ return side * 4;
+});
```
diff --git a/exercises/concept/fruit-picker/.eslintrc b/exercises/concept/fruit-picker/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/concept/fruit-picker/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/concept/fruit-picker/.gitignore b/exercises/concept/fruit-picker/.gitignore
index bdb912f98a..0c88ff6ec3 100644
--- a/exercises/concept/fruit-picker/.gitignore
+++ b/exercises/concept/fruit-picker/.gitignore
@@ -1,3 +1,5 @@
-node_modules
-yarn-error.log
-
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/fruit-picker/babel.config.js b/exercises/concept/fruit-picker/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/concept/fruit-picker/babel.config.js
+++ b/exercises/concept/fruit-picker/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/concept/fruit-picker/eslint.config.mjs b/exercises/concept/fruit-picker/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/fruit-picker/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/fruit-picker/fruit-picker.spec.js b/exercises/concept/fruit-picker/fruit-picker.spec.js
index f920705511..3e1a59bdfd 100644
--- a/exercises/concept/fruit-picker/fruit-picker.spec.js
+++ b/exercises/concept/fruit-picker/fruit-picker.spec.js
@@ -1,8 +1,7 @@
-// @ts-check
-
-import { notify } from './notifier';
-import { order } from './grocer';
+import { afterEach, describe, expect, test, jest } from '@jest/globals';
import { onError, onSuccess, orderFromGrocer, postOrder } from './fruit-picker';
+import { order } from './grocer';
+import { notify } from './notifier';
jest.mock('./notifier', () => ({
notify: jest.fn(),
@@ -33,7 +32,7 @@ describe('task 2', () => {
});
describe('task 3', () => {
- test('order from grocer passes callback function arguments forward', () => {
+ test('orderFromGrocer passes query and callback function arguments forward', () => {
const query = { variety: 'apple', quantity: 10 };
orderFromGrocer(query, onSuccess, onError);
expect(order).toHaveBeenCalledTimes(1);
@@ -51,7 +50,7 @@ describe('task 4', () => {
expect(order).toHaveBeenCalledWith(
{ variety, quantity },
onSuccess,
- onError
+ onError,
);
});
});
diff --git a/exercises/concept/fruit-picker/global.d.ts b/exercises/concept/fruit-picker/global.d.ts
index 19087979c6..0fbffe66e8 100644
--- a/exercises/concept/fruit-picker/global.d.ts
+++ b/exercises/concept/fruit-picker/global.d.ts
@@ -33,7 +33,7 @@ declare module 'grocer' {
function order(
query: GrocerQuery,
onSuccess: GrocerOnSuccessCallback,
- onError: GrocerOnErrorCallback
+ onError: GrocerOnErrorCallback,
): void;
}
diff --git a/exercises/concept/fruit-picker/jest.config.js b/exercises/concept/fruit-picker/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/fruit-picker/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/fruit-picker/package.json b/exercises/concept/fruit-picker/package.json
index d8a444156f..09145fcdb8 100644
--- a/exercises/concept/fruit-picker/package.json
+++ b/exercises/concept/fruit-picker/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/concept/fruit-picker"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/concept/high-score-board/.docs/hints.md b/exercises/concept/high-score-board/.docs/hints.md
index f38a74fb6a..439fd9d8d2 100644
--- a/exercises/concept/high-score-board/.docs/hints.md
+++ b/exercises/concept/high-score-board/.docs/hints.md
@@ -28,11 +28,6 @@
- Use a `for...in` loop to go through all keys in the object.
- For each key, set the new value as you did in task 4.
-## 6. Normalize a high score
-
-- You can access the normalization function like you would access any other key in the object.
-- Then, you can call that function using round brackets and pass in the score as an argument.
-
[mdn-delete]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete
[mdn-shorthand-assignment]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Addition_assignment
[mdn-for-in]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in
diff --git a/exercises/concept/high-score-board/.docs/instructions.md b/exercises/concept/high-score-board/.docs/instructions.md
index 6e04979ccd..7fea2acda0 100644
--- a/exercises/concept/high-score-board/.docs/instructions.md
+++ b/exercises/concept/high-score-board/.docs/instructions.md
@@ -2,7 +2,7 @@
In this exercise, you are implementing a way to keep track of the high scores for the most popular game in your local arcade hall.
-You have 6 functions to implement, mostly related to manipulating an object that holds high scores.
+You have 5 functions to implement, mostly related to manipulating an object that holds high scores.
## 1. Create a new high score board
@@ -87,26 +87,3 @@ const scoreBoard = {
applyMondayBonus(scoreBoard);
// => { 'Dave Thomas': 144, 'Freyja Ćirić': 639, 'José Valim': 365 }
```
-
-## 6. Normalize a high score
-
-Different arcade halls award different score points.
-To celebrate the best arcade player in town, a player's score needs to be normalized so scores from different arcade halls become comparable.
-
-Write a function `normalizeScore`.
-To practice your object skills, instead of two parameters this function should accept one object as a parameter.
-That object contains a key `score` with the value being a player's score (a number).
-There is also a second key `normalizeFunction` that has a function as its value.
-This function takes a score as an argument and returns the corrected score.
-
-Your function `normalizeScore` should return the normalized score that you get after applying the normalization function to the score that was passed in.
-
-```javascript
-function normalize(score) {
- return 2 * score + 10;
-}
-
-const params = { score: 400, normalizeFunction: normalize };
-normalizeScore(params);
-// => 810
-```
diff --git a/exercises/concept/high-score-board/.docs/introduction.md b/exercises/concept/high-score-board/.docs/introduction.md
index a4424c2b57..299ca6a78e 100644
--- a/exercises/concept/high-score-board/.docs/introduction.md
+++ b/exercises/concept/high-score-board/.docs/introduction.md
@@ -4,15 +4,16 @@
Besides primitive data types like `number` and `string`, there is another important data type in JavaScript called `object`.
Objects are collections of key-value pairs.
-As such, they can be used as what is often referred to as maps or dictionaries in other languages.
+As such, they can be used in the same way as what are often referred to as maps or dictionaries in other languages.
In other languages, all values in a map often need to have the same data type.
-In JavaScript, only the type of the key is restricted: it has to be a string.
+In JavaScript, only the type of the key is restricted: it has to be a `string`.
The values inside one object can have different types.
They can be primitive types like numbers but also arrays, other objects or even functions.
This makes objects very versatile so that they are also key entities for [object-oriented programming][oop] (OOP) in JavaScript.
-In the following, we will focus on objects as collections or maps. Other use cases of objects will be covered in other concepts, see e.g., [Classes][concept-classes].
+In the following, we will focus on objects as collections or maps.
+Other use cases of objects will be covered in other concepts, see e.g., [Classes][concept-classes].
## Creating an Object
@@ -143,7 +144,7 @@ for (let key in obj) {
To avoid subtle errors, you should always assume the `for...in` loop visits the keys in an arbitrary order.
Also, be aware that `for...in` includes [inherited keys][concept-inheritance] in its iteration.
-[oop]: https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object-oriented_JS
+[oop]: https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object-oriented_programming
[concept-classes]: /tracks/javascript/concepts/classes
[mdn-identifier]: https://developer.mozilla.org/en-US/docs/Glossary/Identifier
[concept-inheritance]: /tracks/javascript/concepts/inheritance
diff --git a/exercises/concept/high-score-board/.eslintrc b/exercises/concept/high-score-board/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/concept/high-score-board/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/concept/high-score-board/.gitignore b/exercises/concept/high-score-board/.gitignore
index bdb912f98a..0c88ff6ec3 100644
--- a/exercises/concept/high-score-board/.gitignore
+++ b/exercises/concept/high-score-board/.gitignore
@@ -1,3 +1,5 @@
-node_modules
-yarn-error.log
-
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/high-score-board/.meta/design.md b/exercises/concept/high-score-board/.meta/design.md
index 45307215e9..9c425cb787 100644
--- a/exercises/concept/high-score-board/.meta/design.md
+++ b/exercises/concept/high-score-board/.meta/design.md
@@ -40,33 +40,27 @@ The Concepts this exercise unlocks are:
This exercise could benefit from the following rules in the [analyzer][analyzer]:
1. `createScoreBoard`
-
- `essential`: Make sure no class, map etc. was created, there should be just an object.
- `actionable`: If the student created an empty object first and then added the value, give feedback to include the entry in the object literal directly.
- `actionable`: Check that the object was returned directly, no intermediate assignment to a variable necessary.
2. `addPlayer`
-
- `essential`: Check the assignment operator was used and no additional variables were declared.
3. `removePlayer`
-
- `essential`: Make sure `delete` was used and not set to undefined or null.
- `actionable`: If there is additional code to check whether the key is present before deleting it, give feedback that this is not necessary.
4. `updateScore`
-
- `actionable`: If the student used a separate variable to calculate the new value first, tell them it is not necessary.
- `actionable`: If the student did not use the shorthand assignment operator, tell them about it. If they used it already, give a `celebratory` comment.
5. `applyMondayBonus`
-
- `essential`: Check the student actually used `for...in`.
- Same feedback as in `updateScore` applies.
- Using `updateScore` in the solution should be treated as equally correct as the exemplar solution.
6. `normalizeScore`
-
- `actionable`: No intermediate variables necessary.
## Notes
diff --git a/exercises/concept/high-score-board/.meta/exemplar.js b/exercises/concept/high-score-board/.meta/exemplar.js
index c0bc95507f..5db3ee8616 100644
--- a/exercises/concept/high-score-board/.meta/exemplar.js
+++ b/exercises/concept/high-score-board/.meta/exemplar.js
@@ -63,13 +63,3 @@ export function applyMondayBonus(scoreBoard) {
return scoreBoard;
}
-
-/**
- * Normalizes a score with the provided normalization function.
- *
- * @param {Params} params the parameters for performing the normalization
- * @returns {number} normalized score
- */
-export function normalizeScore(params) {
- return params.normalizeFunction(params.score);
-}
diff --git a/exercises/concept/high-score-board/babel.config.js b/exercises/concept/high-score-board/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/concept/high-score-board/babel.config.js
+++ b/exercises/concept/high-score-board/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/concept/high-score-board/eslint.config.mjs b/exercises/concept/high-score-board/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/high-score-board/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/high-score-board/high-score-board.js b/exercises/concept/high-score-board/high-score-board.js
index d9ebe3b8b7..c652240b8f 100644
--- a/exercises/concept/high-score-board/high-score-board.js
+++ b/exercises/concept/high-score-board/high-score-board.js
@@ -7,7 +7,7 @@
* @returns {Record} new score board
*/
export function createScoreBoard() {
- throw new Error('Please implement the createScoreBoard function');
+ throw new Error('Remove this line and implement the function');
}
/**
@@ -19,7 +19,7 @@ export function createScoreBoard() {
* @returns {Record} updated score board
*/
export function addPlayer(scoreBoard, player, score) {
- throw new Error('Please implement the addPlayer function');
+ throw new Error('Remove this line and implement the function');
}
/**
@@ -30,7 +30,7 @@ export function addPlayer(scoreBoard, player, score) {
* @returns {Record} updated score board
*/
export function removePlayer(scoreBoard, player) {
- throw new Error('Please implement the removePlayer function');
+ throw new Error('Remove this line and implement the function');
}
/**
@@ -42,7 +42,7 @@ export function removePlayer(scoreBoard, player) {
* @returns {Record} updated score board
*/
export function updateScore(scoreBoard, player, points) {
- throw new Error('Please implement the updateScore function');
+ throw new Error('Remove this line and implement the function');
}
/**
@@ -52,15 +52,5 @@ export function updateScore(scoreBoard, player, points) {
* @returns {Record} updated score board
*/
export function applyMondayBonus(scoreBoard) {
- throw new Error('Please implement the applyMondayBonus function');
-}
-
-/**
- * Normalizes a score with the provided normalization function.
- *
- * @param {Params} params the parameters for performing the normalization
- * @returns {number} normalized score
- */
-export function normalizeScore(params) {
- throw new Error('Please implement the normalizeScore function');
+ throw new Error('Remove this line and implement the function');
}
diff --git a/exercises/concept/high-score-board/high-score-board.spec.js b/exercises/concept/high-score-board/high-score-board.spec.js
index 9a4f73260e..42f5f67823 100644
--- a/exercises/concept/high-score-board/high-score-board.spec.js
+++ b/exercises/concept/high-score-board/high-score-board.spec.js
@@ -1,10 +1,10 @@
+import { describe, expect, test } from '@jest/globals';
import {
- createScoreBoard,
addPlayer,
+ applyMondayBonus,
+ createScoreBoard,
removePlayer,
updateScore,
- applyMondayBonus,
- normalizeScore,
} from './high-score-board';
describe('createScoreBoard', () => {
@@ -129,27 +129,3 @@ describe('applyMondayBonus', () => {
expect(Object.is(actual, scoreBoard)).toBe(true);
});
});
-
-describe('normalizeScore', () => {
- test('applies the normalization function', () => {
- const params = {
- score: 45,
- normalizeFunction: function (score) {
- return score * 3 - 10;
- },
- };
-
- expect(normalizeScore(params)).toEqual(125);
- });
-
- test('works for different params', () => {
- const params = {
- score: 2100,
- normalizeFunction: function (score) {
- return score / 2 + 100;
- },
- };
-
- expect(normalizeScore(params)).toEqual(1150);
- });
-});
diff --git a/exercises/concept/high-score-board/jest.config.js b/exercises/concept/high-score-board/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/high-score-board/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/high-score-board/package.json b/exercises/concept/high-score-board/package.json
index eeb6eb7dcc..de406babdd 100644
--- a/exercises/concept/high-score-board/package.json
+++ b/exercises/concept/high-score-board/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/concept/high-score-board"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/concept/lasagna-master/.docs/introduction.md b/exercises/concept/lasagna-master/.docs/introduction.md
index 838502caac..0f9469eb68 100644
--- a/exercises/concept/lasagna-master/.docs/introduction.md
+++ b/exercises/concept/lasagna-master/.docs/introduction.md
@@ -58,6 +58,17 @@ function checkNumber(num) {
}
```
+The return value of a function can be stored in a variable.
+
+```javascript
+function sum(x, y) {
+ return x + y;
+}
+
+const total = sum(5, 10);
+// => 15
+```
+
The result of a function that `return`s no value or does not have a `return` statement is `undefined`.
There are no implicit `return`s in JavaScript.
diff --git a/exercises/concept/lasagna-master/.eslintrc b/exercises/concept/lasagna-master/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/concept/lasagna-master/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/concept/lasagna-master/.gitignore b/exercises/concept/lasagna-master/.gitignore
index bdb912f98a..0c88ff6ec3 100644
--- a/exercises/concept/lasagna-master/.gitignore
+++ b/exercises/concept/lasagna-master/.gitignore
@@ -1,3 +1,5 @@
-node_modules
-yarn-error.log
-
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/lasagna-master/babel.config.js b/exercises/concept/lasagna-master/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/concept/lasagna-master/babel.config.js
+++ b/exercises/concept/lasagna-master/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/concept/lasagna-master/eslint.config.mjs b/exercises/concept/lasagna-master/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/lasagna-master/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/lasagna-master/jest.config.js b/exercises/concept/lasagna-master/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/lasagna-master/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/lasagna-master/lasagna-master.spec.js b/exercises/concept/lasagna-master/lasagna-master.spec.js
index fb7879fef9..573007d474 100644
--- a/exercises/concept/lasagna-master/lasagna-master.spec.js
+++ b/exercises/concept/lasagna-master/lasagna-master.spec.js
@@ -1,8 +1,9 @@
+import { describe, expect, test } from '@jest/globals';
import {
+ addSecretIngredient,
cookingStatus,
preparationTime,
quantities,
- addSecretIngredient,
scaleRecipe,
} from './lasagna-master';
@@ -249,7 +250,7 @@ function expectObjectsToBeEqual(actualObj, expectedObj) {
for (const key in expectedObj) {
expect(actualObj[key]).toBeCloseTo(
expectedObj[key],
- DIFFERENCE_PRECISION_IN_DIGITS
+ DIFFERENCE_PRECISION_IN_DIGITS,
);
}
expect(Object.keys(actualObj).length).toBe(Object.keys(expectedObj).length);
diff --git a/exercises/concept/lasagna-master/package.json b/exercises/concept/lasagna-master/package.json
index 441c920e5e..ff12600343 100644
--- a/exercises/concept/lasagna-master/package.json
+++ b/exercises/concept/lasagna-master/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/concept/lasagna-master"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/concept/lasagna/.docs/introduction.md b/exercises/concept/lasagna/.docs/introduction.md
index 2d9cbf3492..f852f68f5c 100644
--- a/exercises/concept/lasagna/.docs/introduction.md
+++ b/exercises/concept/lasagna/.docs/introduction.md
@@ -2,13 +2,18 @@
JavaScript is a dynamic language, supporting object-oriented, imperative, and declarative (e.g. functional programming) styles.
-## (Re-)Assignment
+## Variables
-There are a few primary ways to assign values to names in JavaScript - using variables or constants. On Exercism, variables are always written in [camelCase][wiki-camel-case]; constants are written in [SCREAMING_SNAKE_CASE][wiki-snake-case]. There is no official guide to follow, and various companies and organizations have various style guides. _Feel free to write variables any way you like_. The upside from writing them the way the exercises are prepared is that they'll be highlighted differently in the web interface and most IDEs.
+There are a few primary ways to assign values to names in JavaScript - using variables or constants.
+On Exercism, variables are always written in [camelCase][wiki-camel-case]; constants are written in [SCREAMING_SNAKE_CASE][wiki-snake-case].
+There is no official guide to follow, and various companies and organizations have various style guides.
+_Feel free to write variables any way you like_.
+The upside from writing them the way the exercises are prepared is that they'll be highlighted differently in the web interface and most IDEs.
Variables in JavaScript can be defined using the [`const`][mdn-const], [`let`][mdn-let] or [`var`][mdn-var] keyword.
-A variable can reference different values over its lifetime when using `let` or `var`. For example, `myFirstVariable` can be defined and redefined many times using the assignment operator `=`:
+A variable can reference different values over its lifetime when using `let` or `var`.
+For example, `myFirstVariable` can be defined and redefined many times using the assignment operator `=`:
```javascript
let myFirstVariable = 1;
@@ -16,7 +21,8 @@ myFirstVariable = 'Some string';
myFirstVariable = new SomeComplexClass();
```
-In contrast to `let` and `var`, variables that are defined with `const` can only be assigned once. This is used to define constants in JavaScript.
+In contrast to `let` and `var`, variables that are defined with `const` can only be assigned once.
+This is used to define constants in JavaScript.
```javascript
const MY_FIRST_CONSTANT = 10;
@@ -26,11 +32,16 @@ MY_FIRST_CONSTANT = 20;
// => TypeError: Assignment to constant variable.
```
-> 💡 In a later Concept Exercise the difference between _constant_ assignment / binding and _constant_ value is explored and explained.
+
+~~~exercism/note
+💡 In a later Concept Exercise the difference between _constant_ assignment, _constant_ binding, and _constant_ value is explored and explained.
+~~~
## Function Declarations
-In JavaScript, units of functionality are encapsulated in _functions_, usually grouping functions together in the same file if they belong together. These functions can take parameters (arguments), and can _return_ a value using the `return` keyword. Functions are invoked using `()` syntax.
+In JavaScript, units of functionality are encapsulated in _functions_, usually grouping functions together in the same file if they belong together.
+These functions have parameters (so they can take arguments), and can _return_ a value using the `return` keyword.
+Functions are invoked using `()` syntax.
```javascript
function add(num1, num2) {
@@ -41,11 +52,31 @@ add(1, 3);
// => 4
```
-> 💡 In JavaScript there are _many_ different ways to declare a function. These other ways look different than using the `function` keyword. The track tries to gradually introduce them, but if you already know about them, feel free to use any of them. In most cases, using one or the other isn't better or worse.
+In this example, the function name is `add`.
+It has two parameters, `num1` and `num2`.
+It returns a value: the result of the expression `num1 + num2`
+
+Then the function `add` is _called_ using two arguments: `1` and `3`.
+The result of the function, the returned value, is `4`.
+
+```exercism/note
+💡 In JavaScript there are _many_ different ways to declare a function.
+Some of these other ways look different than using the `function` keyword.
+The track tries to gradually introduce them, but if you already know about them, feel free to use any of them.
+In most cases, using one or the other isn't better or worse.
+```
## Exposing to Other Files
-To make a `function`, a constant, or a variable available in _other files_, they need to be [exported][mdn-export] using the `export` keyword. Another file may then [import][mdn-import] these using the `import` keyword. This is also known as the module system. A great example is how all the tests work. Each exercise has at least one file, for example `lasagna.js`, which contains the _implementation_. Additionally there is at least one other file, for example `lasagna.spec.js`, that contains the _tests_. This file _imports_ the public (i.e. exported) entities in order to test the implementation:
+To make a `function`, a constant, or a variable available in _other files_, they need to be [exported][mdn-export] using the `export` keyword.
+Another file may then [import][mdn-import] these using the `import` keyword.
+This is also known as the module system.
+
+A great example is how all the tests work.
+Each exercise has at least one file, for example `lasagna.js`, which contains the _implementation_.
+The _implementation_ is `export`ed from the file, making it available to import in another file.
+Additionally there is at least one other file, for example `lasagna.spec.js`, that contains the _tests_.
+This file `import`s the public (i.e. `export`ed) entities in order to test the implementation:
```javascript
// file.js
diff --git a/exercises/concept/lasagna/.eslintrc b/exercises/concept/lasagna/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/concept/lasagna/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/concept/lasagna/.gitignore b/exercises/concept/lasagna/.gitignore
index bdb912f98a..0c88ff6ec3 100644
--- a/exercises/concept/lasagna/.gitignore
+++ b/exercises/concept/lasagna/.gitignore
@@ -1,3 +1,5 @@
-node_modules
-yarn-error.log
-
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/lasagna/babel.config.js b/exercises/concept/lasagna/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/concept/lasagna/babel.config.js
+++ b/exercises/concept/lasagna/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/concept/lasagna/eslint.config.mjs b/exercises/concept/lasagna/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/lasagna/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/lasagna/jest.config.js b/exercises/concept/lasagna/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/lasagna/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/lasagna/lasagna.spec.js b/exercises/concept/lasagna/lasagna.spec.js
index 92c02fe58a..243ecf64c0 100644
--- a/exercises/concept/lasagna/lasagna.spec.js
+++ b/exercises/concept/lasagna/lasagna.spec.js
@@ -1,8 +1,8 @@
+import { describe, expect, test } from '@jest/globals';
import {
- // eslint-disable-next-line import/named
EXPECTED_MINUTES_IN_OVEN,
- remainingMinutesInOven,
preparationTimeInMinutes,
+ remainingMinutesInOven,
totalTimeInMinutes,
} from './lasagna';
@@ -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);
});
});
diff --git a/exercises/concept/lasagna/package.json b/exercises/concept/lasagna/package.json
index 722a11d768..0448e795bf 100644
--- a/exercises/concept/lasagna/package.json
+++ b/exercises/concept/lasagna/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/concept/lasagna"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/concept/lucky-numbers/.docs/instructions.md b/exercises/concept/lucky-numbers/.docs/instructions.md
index 4d54b563dc..d71bbd99be 100644
--- a/exercises/concept/lucky-numbers/.docs/instructions.md
+++ b/exercises/concept/lucky-numbers/.docs/instructions.md
@@ -1,7 +1,7 @@
# Instructions
Your friend Kojo is a big fan of numbers.
-He has a small website for playing around with numbers: www.fun-with-numbers.com.
+He has a small website called 'fun-with-numbers'.
Kojo is not that good at programming so he asked you for help.
You will build two helper functions for new number games on Kojos' website and a third one to validate some input the user can enter.
diff --git a/exercises/concept/lucky-numbers/.eslintrc b/exercises/concept/lucky-numbers/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/concept/lucky-numbers/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/concept/lucky-numbers/.gitignore b/exercises/concept/lucky-numbers/.gitignore
index bdb912f98a..0c88ff6ec3 100644
--- a/exercises/concept/lucky-numbers/.gitignore
+++ b/exercises/concept/lucky-numbers/.gitignore
@@ -1,3 +1,5 @@
-node_modules
-yarn-error.log
-
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/lucky-numbers/.meta/exemplar.js b/exercises/concept/lucky-numbers/.meta/exemplar.js
index eddd45dcaa..c4b488f852 100644
--- a/exercises/concept/lucky-numbers/.meta/exemplar.js
+++ b/exercises/concept/lucky-numbers/.meta/exemplar.js
@@ -18,7 +18,7 @@ export function twoSum(array1, array2) {
* Checks whether a number is a palindrome.
*
* @param {number} value
- * @returns {boolean} whether the number is a palindrome or not
+ * @returns {boolean} whether the number is a palindrome or not
*/
export function luckyNumber(value) {
const strValue = String(value);
diff --git a/exercises/concept/lucky-numbers/babel.config.js b/exercises/concept/lucky-numbers/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/concept/lucky-numbers/babel.config.js
+++ b/exercises/concept/lucky-numbers/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/concept/lucky-numbers/eslint.config.mjs b/exercises/concept/lucky-numbers/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/lucky-numbers/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/lucky-numbers/jest.config.js b/exercises/concept/lucky-numbers/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/lucky-numbers/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/lucky-numbers/lucky-numbers.js b/exercises/concept/lucky-numbers/lucky-numbers.js
index 2f5d931f1b..3146f9154b 100644
--- a/exercises/concept/lucky-numbers/lucky-numbers.js
+++ b/exercises/concept/lucky-numbers/lucky-numbers.js
@@ -8,17 +8,17 @@
* @returns {number} sum of the two arrays
*/
export function twoSum(array1, array2) {
- throw new Error('Implement the twoSum function');
+ throw new Error('Remove this line and implement the function');
}
/**
* Checks whether a number is a palindrome.
*
* @param {number} value
- * @returns {boolean} whether the number is a palindrome or not
+ * @returns {boolean} whether the number is a palindrome or not
*/
export function luckyNumber(value) {
- throw new Error('Implement the luckyNumber function');
+ throw new Error('Remove this line and implement the function');
}
/**
@@ -29,5 +29,5 @@ export function luckyNumber(value) {
* @returns {string} error message
*/
export function errorMessage(input) {
- throw new Error('Implement the errorMessage function');
+ throw new Error('Remove this line and implement the function');
}
diff --git a/exercises/concept/lucky-numbers/lucky-numbers.spec.js b/exercises/concept/lucky-numbers/lucky-numbers.spec.js
index 801bc6d54b..098c881f06 100644
--- a/exercises/concept/lucky-numbers/lucky-numbers.spec.js
+++ b/exercises/concept/lucky-numbers/lucky-numbers.spec.js
@@ -1,4 +1,5 @@
-import { twoSum, luckyNumber, errorMessage } from './lucky-numbers';
+import { describe, expect, test } from '@jest/globals';
+import { errorMessage, luckyNumber, twoSum } from './lucky-numbers';
describe('twoSum', () => {
test('sums the numbers correctly for short arrays', () => {
diff --git a/exercises/concept/lucky-numbers/package.json b/exercises/concept/lucky-numbers/package.json
index f60d9e3bd5..d69ed42637 100644
--- a/exercises/concept/lucky-numbers/package.json
+++ b/exercises/concept/lucky-numbers/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/concept/lucky-numbers"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/concept/mixed-juices/.docs/hints.md b/exercises/concept/mixed-juices/.docs/hints.md
index b44bd7498b..218134f715 100644
--- a/exercises/concept/mixed-juices/.docs/hints.md
+++ b/exercises/concept/mixed-juices/.docs/hints.md
@@ -23,7 +23,7 @@
- You can combine two conditions for the loop using [logical operators][concept-booleans].
[mdn-switch]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/switch#
-[mdn-fallthrough]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/switch#what_happens_if_i_forgot_a_break
+[mdn-fallthrough]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/switch#breaking_and_fall-through
[mdn-while]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/while
[mdn-do-while]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/do...while
[concept-booleans]: /tracks/javascript/concepts/booleans
diff --git a/exercises/concept/mixed-juices/.docs/instructions.md b/exercises/concept/mixed-juices/.docs/instructions.md
index b0e4efbf80..56ea47e264 100644
--- a/exercises/concept/mixed-juices/.docs/instructions.md
+++ b/exercises/concept/mixed-juices/.docs/instructions.md
@@ -50,6 +50,7 @@ To make the hand-over easier, implement a function `remainingOrders` which takes
The function should return the orders that Li Mei cannot start preparing before the end of her workday.
The time left in the shift will always be greater than 0.
+The array of juices to prepare will never be empty.
Furthermore, the orders are prepared in the order in which they appear in the array.
If Li Mei starts to mix a certain juice, she will always finish it even if she has to work a bit longer.
If there are no remaining orders left that Dmitry needs to take care of, an empty array should be returned.
diff --git a/exercises/concept/mixed-juices/.eslintrc b/exercises/concept/mixed-juices/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/concept/mixed-juices/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/concept/mixed-juices/.gitignore b/exercises/concept/mixed-juices/.gitignore
index bdb912f98a..0c88ff6ec3 100644
--- a/exercises/concept/mixed-juices/.gitignore
+++ b/exercises/concept/mixed-juices/.gitignore
@@ -1,3 +1,5 @@
-node_modules
-yarn-error.log
-
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/mixed-juices/.meta/design.md b/exercises/concept/mixed-juices/.meta/design.md
index 5eb2a9b48c..de569e6e9e 100644
--- a/exercises/concept/mixed-juices/.meta/design.md
+++ b/exercises/concept/mixed-juices/.meta/design.md
@@ -38,7 +38,6 @@ This exercise could benefit from the following rules in the [analyzer][analyzer]
The comment types mentioned below only serve as a proposal.
1. `timeToMixJuice`
-
- `essential`: Verify the student used a switch statement.
Would be nice if we could give different feedback depending on what the student used instead.
If it was if-else, comment that switch is better suited for so many different variants.
@@ -53,7 +52,6 @@ The comment types mentioned below only serve as a proposal.
```
2. `limesToCut`
-
- A solution that uses `if (limes.length < 0) break;` instead of combining the conditions should be considered equally correct to the exemplar solution.
The version in the exemplar file is shorter but the break version emphasizes that there is a special edge case.
- `essential`: Verify that `while` was used.
@@ -68,7 +66,6 @@ The comment types mentioned below only serve as a proposal.
- `celebratory`: Celebrate if the student used `++` and `+=`.
3. `remainingOrders`
-
- `essential`: Verify that do-while was used.
If while was used instead, say that do-while is a better fit because there is always at least one iteration (because `timeLeft` is always > 0) and the condition can best be checked after running the code.
- `essential`: Verify `timeToMixJuice` was reused instead of duplicating the code.
diff --git a/exercises/concept/mixed-juices/babel.config.js b/exercises/concept/mixed-juices/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/concept/mixed-juices/babel.config.js
+++ b/exercises/concept/mixed-juices/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/concept/mixed-juices/eslint.config.mjs b/exercises/concept/mixed-juices/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/mixed-juices/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/mixed-juices/jest.config.js b/exercises/concept/mixed-juices/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/mixed-juices/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/mixed-juices/mixed-juices.js b/exercises/concept/mixed-juices/mixed-juices.js
index dde696fac8..d5f175fbe6 100644
--- a/exercises/concept/mixed-juices/mixed-juices.js
+++ b/exercises/concept/mixed-juices/mixed-juices.js
@@ -11,7 +11,7 @@
* @returns {number} time in minutes
*/
export function timeToMixJuice(name) {
- throw new Error('Please implement the timeToMixJuice function');
+ throw new Error('Remove this line and implement the function');
}
/**
@@ -23,7 +23,7 @@ export function timeToMixJuice(name) {
* @returns {number} number of limes cut
*/
export function limesToCut(wedgesNeeded, limes) {
- throw new Error('Please implement the limesToCut function');
+ throw new Error('Remove this line and implement the function');
}
/**
@@ -34,5 +34,5 @@ export function limesToCut(wedgesNeeded, limes) {
* @returns {string[]} remaining orders after the time is up
*/
export function remainingOrders(timeLeft, orders) {
- throw new Error('Please implement the remainingOrders function');
+ throw new Error('Remove this line and implement the function');
}
diff --git a/exercises/concept/mixed-juices/mixed-juices.spec.js b/exercises/concept/mixed-juices/mixed-juices.spec.js
index 3dbb303149..64eeea52c1 100644
--- a/exercises/concept/mixed-juices/mixed-juices.spec.js
+++ b/exercises/concept/mixed-juices/mixed-juices.spec.js
@@ -1,4 +1,5 @@
-import { timeToMixJuice, limesToCut, remainingOrders } from './mixed-juices';
+import { describe, expect, test } from '@jest/globals';
+import { limesToCut, remainingOrders, timeToMixJuice } from './mixed-juices';
describe('timeToMixJuice', () => {
test("returns the correct time for 'Pure Strawberry Joy'", () => {
diff --git a/exercises/concept/mixed-juices/package.json b/exercises/concept/mixed-juices/package.json
index c005c961d2..b25b726247 100644
--- a/exercises/concept/mixed-juices/package.json
+++ b/exercises/concept/mixed-juices/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/concept/mixed-juices"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/concept/nullability/.eslintrc b/exercises/concept/nullability/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/concept/nullability/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/concept/nullability/.gitignore b/exercises/concept/nullability/.gitignore
index bdb912f98a..0c88ff6ec3 100644
--- a/exercises/concept/nullability/.gitignore
+++ b/exercises/concept/nullability/.gitignore
@@ -1,3 +1,5 @@
-node_modules
-yarn-error.log
-
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/nullability/babel.config.js b/exercises/concept/nullability/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/concept/nullability/babel.config.js
+++ b/exercises/concept/nullability/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/concept/nullability/eslint.config.mjs b/exercises/concept/nullability/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/nullability/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/nullability/jest.config.js b/exercises/concept/nullability/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/nullability/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/nullability/nullability.js b/exercises/concept/nullability/nullability.js
index b8ead1ec6f..7638e2a5de 100644
--- a/exercises/concept/nullability/nullability.js
+++ b/exercises/concept/nullability/nullability.js
@@ -14,5 +14,5 @@
* @returns {string} the text to print on the badge
*/
export function printBadge(id, name, department) {
- throw new Error('Please implement the printBadge function');
+ throw new Error('Remove this line and implement the function');
}
diff --git a/exercises/concept/nullability/nullability.spec.js b/exercises/concept/nullability/nullability.spec.js
index a6cf6233e1..95944bb5e3 100644
--- a/exercises/concept/nullability/nullability.spec.js
+++ b/exercises/concept/nullability/nullability.spec.js
@@ -1,29 +1,30 @@
+import { describe, expect, test } from '@jest/globals';
import { printBadge } from './nullability';
describe('nullability', () => {
describe('printBadge', () => {
- it("printBadge(17, 'Ryder Herbert', 'Marketing')", () => {
+ test("printBadge(17, 'Ryder Herbert', 'Marketing')", () => {
const actual = printBadge(17, 'Ryder Herbert', 'Marketing');
expect(actual).toBe('[17] Ryder Herbert - MARKETING');
});
});
describe('printBadge without an employee ID', () => {
- it("printBadge(null, 'Bogdan Rosario', 'Marketing')", () => {
+ test("printBadge(null, 'Bogdan Rosario', 'Marketing')", () => {
const actual = printBadge(null, 'Bogdan Rosario', 'Marketing');
expect(actual).toBe('Bogdan Rosario - MARKETING');
});
});
describe('printBadge without a department', () => {
- it("printBadge(59, 'Julie Sokato', null)", () => {
+ test("printBadge(59, 'Julie Sokato', null)", () => {
const actual = printBadge(59, 'Julie Sokato', null);
expect(actual).toBe('[59] Julie Sokato - OWNER');
});
});
describe('printBadge for a new owner', () => {
- it("printBadge(null, 'Amare Osei', null)", () => {
+ test("printBadge(null, 'Amare Osei', null)", () => {
const actual = printBadge(null, 'Amare Osei', null);
expect(actual).toBe('Amare Osei - OWNER');
});
diff --git a/exercises/concept/nullability/package.json b/exercises/concept/nullability/package.json
index e8d00b6858..fddaed4c1a 100644
--- a/exercises/concept/nullability/package.json
+++ b/exercises/concept/nullability/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/concept/nullability"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/concept/ozans-playlist/.docs/introduction.md b/exercises/concept/ozans-playlist/.docs/introduction.md
index e7f8a3c2d8..2a5c914459 100644
--- a/exercises/concept/ozans-playlist/.docs/introduction.md
+++ b/exercises/concept/ozans-playlist/.docs/introduction.md
@@ -25,5 +25,31 @@ console.log(set.size);
//=> 4
```
+You can provide an array as an argument when creating a set, and the array's values will become the values of the set, also removing the duplicate values.
+
+```javascript
+const array = [1, 5, 4, 1];
+const set = new Set(array); // the set's values become [1, 5, 4]
+
+console.log(set.size);
+//=> 3
+```
+
+To convert a set to an array, you can use [Array.from()][mdn-array-from], which converts an iterable such as a set or a map to an array.
+
+```javascript
+const set = new Set();
+
+set.add(1);
+set.add(2);
+set.add(3);
+set.add(4);
+
+const array = Array.from(set);
+console.log(array);
+//=> [1, 2, 3, 4]
+```
+
[mdn-sets]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set
[mdn-strict-equality]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness#strict_equality_using
+[mdn-array-from]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from
diff --git a/exercises/concept/ozans-playlist/.eslintrc b/exercises/concept/ozans-playlist/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/concept/ozans-playlist/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/concept/ozans-playlist/.gitignore b/exercises/concept/ozans-playlist/.gitignore
index bdb912f98a..0c88ff6ec3 100644
--- a/exercises/concept/ozans-playlist/.gitignore
+++ b/exercises/concept/ozans-playlist/.gitignore
@@ -1,3 +1,5 @@
-node_modules
-yarn-error.log
-
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/ozans-playlist/.meta/design.md b/exercises/concept/ozans-playlist/.meta/design.md
index 79f9bc0f1e..0ae1f98a0f 100644
--- a/exercises/concept/ozans-playlist/.meta/design.md
+++ b/exercises/concept/ozans-playlist/.meta/design.md
@@ -35,11 +35,9 @@ This exercise could benefit from the following rules in the [analyzer][analyzer]
For all tasks, verify that the student actually used a `Set`.
1. `addTrack`
-
- Verify that there was no redundant `Set.has()` call
2. `deleteTrack`
-
- Verify that there was no redundant `Set.has()` call
[analyzer]: https://github.com/exercism/javascript-analyzer
diff --git a/exercises/concept/ozans-playlist/babel.config.js b/exercises/concept/ozans-playlist/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/concept/ozans-playlist/babel.config.js
+++ b/exercises/concept/ozans-playlist/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/concept/ozans-playlist/eslint.config.mjs b/exercises/concept/ozans-playlist/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/ozans-playlist/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/ozans-playlist/jest.config.js b/exercises/concept/ozans-playlist/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/ozans-playlist/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/ozans-playlist/ozans-playlist.js b/exercises/concept/ozans-playlist/ozans-playlist.js
index 349cdd5f04..1b02443a3a 100644
--- a/exercises/concept/ozans-playlist/ozans-playlist.js
+++ b/exercises/concept/ozans-playlist/ozans-playlist.js
@@ -11,7 +11,7 @@
* @returns {string[]} new playlist with unique entries
*/
export function removeDuplicates(playlist) {
- throw new Error('Please implement the removeDuplicates function');
+ throw new Error('Remove this line and implement the function');
}
/**
@@ -22,7 +22,7 @@ export function removeDuplicates(playlist) {
* @returns {boolean} whether the track is in the playlist
*/
export function hasTrack(playlist, track) {
- throw new Error('Please implement the hasTrack function');
+ throw new Error('Remove this line and implement the function');
}
/**
@@ -33,7 +33,7 @@ export function hasTrack(playlist, track) {
* @returns {string[]} new playlist
*/
export function addTrack(playlist, track) {
- throw new Error('Please implement the addTrack function');
+ throw new Error('Remove this line and implement the function');
}
/**
@@ -44,7 +44,7 @@ export function addTrack(playlist, track) {
* @returns {string[]} new playlist
*/
export function deleteTrack(playlist, track) {
- throw new Error('Please implement the deleteTrack function');
+ throw new Error('Remove this line and implement the function');
}
/**
@@ -54,5 +54,5 @@ export function deleteTrack(playlist, track) {
* @returns {string[]} list of artists
*/
export function listArtists(playlist) {
- throw new Error('Please implement the listArtists function');
+ throw new Error('Remove this line and implement the function');
}
diff --git a/exercises/concept/ozans-playlist/ozans-playlist.spec.js b/exercises/concept/ozans-playlist/ozans-playlist.spec.js
index d613498d5d..5047bd1618 100644
--- a/exercises/concept/ozans-playlist/ozans-playlist.spec.js
+++ b/exercises/concept/ozans-playlist/ozans-playlist.spec.js
@@ -1,3 +1,4 @@
+import { describe, expect, test } from '@jest/globals';
import {
addTrack,
deleteTrack,
diff --git a/exercises/concept/ozans-playlist/package.json b/exercises/concept/ozans-playlist/package.json
index 7e8ec2759c..eda60e6394 100644
--- a/exercises/concept/ozans-playlist/package.json
+++ b/exercises/concept/ozans-playlist/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/concept/ozans-playlist"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/concept/pizza-order/.docs/hints.md b/exercises/concept/pizza-order/.docs/hints.md
index b5296c3664..f938b628ed 100644
--- a/exercises/concept/pizza-order/.docs/hints.md
+++ b/exercises/concept/pizza-order/.docs/hints.md
@@ -1 +1,13 @@
# Hints
+
+## 1. Calculate the price of a pizza
+
+- Set up a recursive function that returns the price of one extra option added to the price of the pizza without that extra topping.
+- If `Maximum call stack size exceeded` error is raised, check if you've missed out on the base case.
+- If you're getting a `Memory Allocation Error`, check if you're duplicating the input array of toppings instead of mutating it.
+
+## 2. Calculate the total price of an order
+
+- Using `pizzaPrice`, calculate the price of each pizza in the array, and add it to a sum.
+- If `Maximum call stack size exceeded` error is raised, and more than one test is failing, check if you've missed out on the base case. If only one test is failing, try implementing this with a loop!
+- If you're getting a `Memory Allocation Error`, check if you're duplicating the input array instead of mutating it.
diff --git a/exercises/concept/pizza-order/.docs/instructions.md b/exercises/concept/pizza-order/.docs/instructions.md
index 63fd98ab00..0ae66b16ca 100644
--- a/exercises/concept/pizza-order/.docs/instructions.md
+++ b/exercises/concept/pizza-order/.docs/instructions.md
@@ -1 +1,89 @@
# Instructions
+
+You run a pizza shop, and offer three types of pizzas:
+
+- Margherita: $7
+- Caprese: $9
+- Formaggio: $10
+
+If customers want, they can add an unlimited number of extra options: either "ExtraSauce" for $1 or "ExtraToppings" for $2.
+
+Your task is to write code that assists the customer in figuring out the cost to them.
+
+## Calculate the price of a pizza
+
+Provided the pizza's name as the first argument, and an unlimited number of added options, calculate the price of the pizza in dollars.
+
+```js
+pizzaPrice('Margherita');
+// => 7
+
+pizzaPrice('Caprese', 'ExtraSauce', 'ExtraToppings');
+// => 12
+
+pizzaPrice(
+ 'Caprese',
+ 'ExtraToppings',
+ 'ExtraToppings',
+ 'ExtraToppings',
+ 'ExtraToppings',
+);
+// => 17
+```
+
+## Calculate the total price of an order
+
+Your function is called with a list of `PizzaOrder`s and should return the total price of the order in dollars.
+Each `PizzaOrder` has a `pizza` property - the pizza's name, and an `extras` property - the list of extra options.
+
+```js
+const margherita = new PizzaOrder('Margherita');
+const caprese = new PizzaOrder('Caprese', 'ExtraToppings');
+orderPrice([margherita, caprese]);
+// => 18
+```
+
+You'll realize that you can't write this using recursion, as one test with a tremendous amount of orders will raise a `Maximum call stack size exceeded`.
+No worries, this is intentional - try implementing this function using an imperative loop!
+You have many options, such as, but not limited to using `reduce` or a `for` loop.
+
+
+~~~~exercism/advanced
+When the JavaScript interpreter is running the JavaScript code, it will keep track of which functions it has entered (started to call) on a data structure called "a stack".
+When the function returns (ends), it is removed from the stack.
+
+However, this stack has a limited size.
+The most common mistake made is a recursive function that never ends.
+Each call is placed on the stack, but before it returns, another call is placed on the stack.
+
+```javascript
+function kaboom() {
+ kaboom()
+}
+
+kaboom()
+// => RangeError: Maximum call stack size exceeded
+```
+
+The stacktrace of this error shows the same line over and over, which makes sense, because the function calls itself.
+Whilst it has no real practical application in most cases, you can find out how tall that stack can get.
+
+```javascript
+let calls = 0;
+function kaboom() {
+ calls +=1 ;
+ kaboom()
+}
+
+kaboom()
+// => RangeError: Maximum call stack size exceeded
+
+console.log(calls)
+// => a number, generally higher than 10.000
+```
+
+There are only two viable solutions to a call stack error caused by a synchronous recursive function:
+- ensure the functions return before the stack limit is reached, usually by adding or fixing a base case.
+- rewrite the recursive function to an imperative loop, which will execute the body of the loop, without having to enter a function, thus without increasing the stack.
+~~~~
+
diff --git a/exercises/concept/pizza-order/.docs/introduction.md b/exercises/concept/pizza-order/.docs/introduction.md
index e10b99d013..8a4d726b7d 100644
--- a/exercises/concept/pizza-order/.docs/introduction.md
+++ b/exercises/concept/pizza-order/.docs/introduction.md
@@ -1 +1,93 @@
-# Introduction
+# Understanding Recursion in JavaScript
+
+Recursion is a powerful concept in programming that involves a function calling itself.
+It can be a bit tricky to grasp at first, but once you understand the fundamentals, it becomes a valuable tool in solving complex problems.
+In this tutorial, we'll explore recursion in JavaScript with easy-to-understand examples.
+
+## What is Recursion?
+
+Recursion occurs when a function calls itself, either directly or indirectly.
+It's similar to a loop, but it involves breaking a problem down into smaller, more manageable sub-problems.
+
+### Example 1: Countdown
+
+Let's start with a simple example: a countdown function.
+
+```javascript
+function countdown(num) {
+ // Base case
+ if (num <= 0) {
+ console.log('Blastoff!');
+ return;
+ }
+
+ // Recursive case
+ console.log(num);
+ countdown(num - 1);
+}
+
+// Call the function
+countdown(5);
+```
+
+In this example:
+
+- **Base case**: When `num` becomes less than or equal to 0, the function prints "Blastoff!" and stops calling itself.
+- **Recursive case**: The function prints the current `num` and calls itself with `num - 1`.
+
+### Example 2: Factorial
+
+Now, let's look at a classic example of recursion: calculating the factorial of a number.
+
+```javascript
+function factorial(n) {
+ // Base case
+ if (n === 0 || n === 1) {
+ return 1;
+ }
+
+ // Recursive case
+ return n * factorial(n - 1);
+}
+
+// Test the function
+console.log(factorial(5)); // Output: 120
+```
+
+In this example:
+
+- **Base case**: When `n` is 0 or 1, the function returns 1.
+- **Recursive case**: The function multiplies `n` by the factorial of `n - 1`.
+
+## Key Concepts
+
+### Base Case
+
+Every recursive function must have at least one base case, a condition where the function stops calling itself.
+Without a base case, the recursion would continue indefinitely, leading to a stack overflow.
+
+### Recursive Case
+
+The recursive case defines how the function calls itself with a smaller or simpler version of the problem.
+
+## Pros and Cons of Recursion
+
+**Pros:**
+
+- Elegant solution for certain problems.
+- Mimics the mathematical induction concept.
+
+**Cons:**
+
+- Can be less efficient than iterative solutions.
+- May lead to stack overflow for deep recursion.
+
+## Conclusion
+
+Recursion is a valuable technique that can simplify complex problems by breaking them into smaller, more manageable sub-problems.
+Understanding base cases and recursive cases is crucial for implementing effective recursive solutions in JavaScript.
+
+**Learn More:**
+
+- [MDN: Recursion in JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions#recursion)
+- [Eloquent JavaScript: Chapter 3 - Functions](https://eloquentjavascript.net/03_functions.html)
diff --git a/exercises/concept/pizza-order/.eslintrc b/exercises/concept/pizza-order/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/concept/pizza-order/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/concept/pizza-order/.gitignore b/exercises/concept/pizza-order/.gitignore
index bdb912f98a..0c88ff6ec3 100644
--- a/exercises/concept/pizza-order/.gitignore
+++ b/exercises/concept/pizza-order/.gitignore
@@ -1,3 +1,5 @@
-node_modules
-yarn-error.log
-
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/pizza-order/.meta/config.json b/exercises/concept/pizza-order/.meta/config.json
index 38818be8cf..ecacc4de6e 100644
--- a/exercises/concept/pizza-order/.meta/config.json
+++ b/exercises/concept/pizza-order/.meta/config.json
@@ -1,6 +1,10 @@
{
"authors": [
- "SleeplessByte"
+ "SleeplessByte",
+ "safwansamsudeen"
+ ],
+ "contributors": [
+ "Cool-Katt"
],
"files": {
"solution": [
@@ -11,12 +15,15 @@
],
"exemplar": [
".meta/exemplar.js"
+ ],
+ "editor": [
+ "global.d.ts"
]
},
"forked_from": [
"fsharp/pizza-pricing"
],
- "blurb": "TODO: add blurb for recursion exercise",
+ "blurb": "Allow customers to calculate the cost of their order",
"custom": {
"version.tests.compatibility": "jest-27",
"flag.tests.task-per-describe": false,
diff --git a/exercises/concept/pizza-order/.meta/exemplar.js b/exercises/concept/pizza-order/.meta/exemplar.js
index 2eba3fed43..31ac0d6ba7 100644
--- a/exercises/concept/pizza-order/.meta/exemplar.js
+++ b/exercises/concept/pizza-order/.meta/exemplar.js
@@ -41,6 +41,6 @@ export function pizzaPrice(pizza, ...[extra, ...otherExtras]) {
export function orderPrice(pizzaOrders) {
return pizzaOrders.reduce(
(result, order) => result + pizzaPrice(order.pizza, ...order.extras),
- 0
+ 0,
);
}
diff --git a/exercises/concept/pizza-order/babel.config.js b/exercises/concept/pizza-order/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/concept/pizza-order/babel.config.js
+++ b/exercises/concept/pizza-order/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/concept/pizza-order/eslint.config.mjs b/exercises/concept/pizza-order/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/pizza-order/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/pizza-order/jest.config.js b/exercises/concept/pizza-order/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/pizza-order/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/pizza-order/package.json b/exercises/concept/pizza-order/package.json
index 35b6cad369..c2d3ad2f06 100644
--- a/exercises/concept/pizza-order/package.json
+++ b/exercises/concept/pizza-order/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/concept/pizza-order"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/concept/pizza-order/pizza-order.js b/exercises/concept/pizza-order/pizza-order.js
index c36d646138..2bbb25f2f3 100644
--- a/exercises/concept/pizza-order/pizza-order.js
+++ b/exercises/concept/pizza-order/pizza-order.js
@@ -3,7 +3,7 @@
// @ts-check
/**
- * Determine the prize of the pizza given the pizza and optional extras
+ * Determine the price of the pizza given the pizza and optional extras
*
* @param {Pizza} pizza name of the pizza to be made
* @param {Extra[]} extras list of extras
@@ -11,15 +11,18 @@
* @returns {number} the price of the pizza
*/
export function pizzaPrice(pizza, ...extras) {
- throw new Error('Please implement the pizzaPrice function');
+ throw new Error('Remove this line and implement the function');
}
/**
- * Calculate the prize of the total order, given individual orders
+ * Calculate the price of the total order, given individual orders
+ *
+ * (HINT: For this exercise, you can take a look at the supplied "global.d.ts" file
+ * for a more info about the type definitions used)
*
* @param {PizzaOrder[]} pizzaOrders a list of pizza orders
* @returns {number} the price of the total order
*/
export function orderPrice(pizzaOrders) {
- throw new Error('Please implement the orderPrice function');
+ throw new Error('Remove this line and implement the function');
}
diff --git a/exercises/concept/pizza-order/pizza-order.spec.js b/exercises/concept/pizza-order/pizza-order.spec.js
index 98c8adf86f..58c7677028 100644
--- a/exercises/concept/pizza-order/pizza-order.spec.js
+++ b/exercises/concept/pizza-order/pizza-order.spec.js
@@ -1,4 +1,5 @@
-import { pizzaPrice, orderPrice } from './pizza-order';
+import { describe, expect, test } from '@jest/globals';
+import { orderPrice, pizzaPrice } from './pizza-order';
class PizzaOrder {
/**
@@ -15,83 +16,83 @@ class PizzaOrder {
}
describe('Price for pizza margherita', () => {
- it("pizzaPrice('Margherita')", () => {
+ test("pizzaPrice('Margherita')", () => {
expect(pizzaPrice('Margherita')).toBe(7);
});
});
describe('Price for pizza formaggio', () => {
- it("pizzaPrice('Formaggio')", () => {
+ test("pizzaPrice('Formaggio')", () => {
expect(pizzaPrice('Formaggio')).toBe(10);
});
});
describe('Price for pizza caprese', () => {
- it("pizzaPrice('Caprese')", () => {
+ test("pizzaPrice('Caprese')", () => {
expect(pizzaPrice('Caprese')).toBe(9);
});
});
describe('Price for pizza margherita with extra sauce', () => {
- it("pizzaPrice('Margherita', 'ExtraSauce')", () => {
+ test("pizzaPrice('Margherita', 'ExtraSauce')", () => {
expect(pizzaPrice('Margherita', 'ExtraSauce')).toBe(8);
});
});
describe('Price for pizza caprese with extra toppings', () => {
- it("pizzaPrice('Caprese', 'ExtraToppings')", () => {
+ test("pizzaPrice('Caprese', 'ExtraToppings')", () => {
expect(pizzaPrice('Caprese', 'ExtraToppings')).toBe(11);
});
});
describe('Price for pizza formaggio with extra sauce and toppings', () => {
- it("pizzaPrice('Formaggio', 'ExtraSauce', 'ExtraToppings')", () => {
+ test("pizzaPrice('Formaggio', 'ExtraSauce', 'ExtraToppings')", () => {
expect(pizzaPrice('Formaggio', 'ExtraSauce', 'ExtraToppings')).toBe(13);
});
});
describe('Price for pizza caprese with extra sauce and toppings', () => {
- it("pizzaPrice('Caprese', 'ExtraSauce', 'ExtraToppings')", () => {
+ test("pizzaPrice('Caprese', 'ExtraSauce', 'ExtraToppings')", () => {
expect(pizzaPrice('Caprese', 'ExtraSauce', 'ExtraToppings')).toBe(12);
});
});
describe('Price for pizza caprese with a lot of extra toppings', () => {
- it("pizzaPrice('Caprese', 'ExtraToppings', 'ExtraToppings', 'ExtraToppings', 'ExtraToppings')", () => {
+ test("pizzaPrice('Caprese', 'ExtraToppings', 'ExtraToppings', 'ExtraToppings', 'ExtraToppings')", () => {
expect(
pizzaPrice(
'Caprese',
'ExtraToppings',
'ExtraToppings',
'ExtraToppings',
- 'ExtraToppings'
- )
+ 'ExtraToppings',
+ ),
).toBe(17);
});
});
describe('Order price for no pizzas', () => {
- it('orderPrice([])', () => {
+ test('orderPrice([])', () => {
expect(orderPrice([])).toBe(0);
});
});
describe('Order price for a single pizza caprese', () => {
- it("orderPrice([PizzaOrder('Caprese')])", () => {
+ test("orderPrice([PizzaOrder('Caprese')])", () => {
const order = new PizzaOrder('Caprese');
expect(orderPrice([order])).toBe(9);
});
});
describe('Order price for a single pizza formaggio with extra sauce', () => {
- it("orderPrice([PizzaOrder('Formaggio', 'ExtraSauce')])", () => {
+ test("orderPrice([PizzaOrder('Formaggio', 'ExtraSauce')])", () => {
const order = new PizzaOrder('Formaggio', 'ExtraSauce');
expect(orderPrice([order])).toBe(11);
});
});
describe('Order price for one pizza margherita and one pizza caprese with extra toppings', () => {
- it("orderPrice([PizzaOrder('Margherita'), PizzaOrder('Caprese', 'ExtraToppings')])", () => {
+ test("orderPrice([PizzaOrder('Margherita'), PizzaOrder('Caprese', 'ExtraToppings')])", () => {
const margherita = new PizzaOrder('Margherita');
const caprese = new PizzaOrder('Caprese', 'ExtraToppings');
@@ -103,19 +104,19 @@ describe('Order price for one pizza margherita and one pizza caprese with extra
});
describe('Order price for one pizza margherita with a LOT of sauce and one pizza caprese with a LOT of toppings', () => {
- it("orderPrice([PizzaOrder('Margherita', 'ExtraSauce', 'ExtraSauce', 'ExtraSauce'), PizzaOrder('Caprese', 'ExtraToppings', 'ExtraToppings', 'ExtraToppings', 'ExtraToppings')])", () => {
+ test("orderPrice([PizzaOrder('Margherita', 'ExtraSauce', 'ExtraSauce', 'ExtraSauce'), PizzaOrder('Caprese', 'ExtraToppings', 'ExtraToppings', 'ExtraToppings', 'ExtraToppings')])", () => {
const saucyMargherita = new PizzaOrder(
'Margherita',
'ExtraSauce',
'ExtraSauce',
- 'ExtraSauce'
+ 'ExtraSauce',
);
const toppedCaprese = new PizzaOrder(
'Caprese',
'ExtraToppings',
'ExtraToppings',
'ExtraToppings',
- 'ExtraToppings'
+ 'ExtraToppings',
);
expect(orderPrice([saucyMargherita, toppedCaprese])).toBe(27);
@@ -126,7 +127,7 @@ describe('Order price for one pizza margherita with a LOT of sauce and one pizza
});
describe('Order price for very large order', () => {
- it('orderPrice([/* lots of */])', () => {
+ test('orderPrice([/* lots of */])', () => {
const margherita = new PizzaOrder('Margherita');
const margherita2 = new PizzaOrder('Margherita', 'ExtraSauce');
const caprese = new PizzaOrder('Caprese');
@@ -136,14 +137,14 @@ describe('Order price for very large order', () => {
const formaggio3 = new PizzaOrder(
'Formaggio',
'ExtraSauce',
- 'ExtraToppings'
+ 'ExtraToppings',
);
const formaggio4 = new PizzaOrder(
'Formaggio',
'ExtraToppings',
'ExtraSauce',
'ExtraToppings',
- 'ExtraSauce'
+ 'ExtraSauce',
);
const actual = orderPrice([
@@ -161,9 +162,9 @@ describe('Order price for very large order', () => {
});
describe('Order price for a gigantic order', () => {
- it('orderPrice([/* lots of */])', () => {
+ test('orderPrice([/* lots of */])', () => {
const allTheMargheritas = Array(100 * 1000).fill(
- new PizzaOrder('Margherita')
+ new PizzaOrder('Margherita'),
);
const actual = orderPrice(allTheMargheritas);
expect(actual).toBe(700 * 1000);
diff --git a/exercises/concept/poetry-club-door-policy/.approaches/config.json b/exercises/concept/poetry-club-door-policy/.approaches/config.json
new file mode 100644
index 0000000000..0b90d0d0dc
--- /dev/null
+++ b/exercises/concept/poetry-club-door-policy/.approaches/config.json
@@ -0,0 +1,7 @@
+{
+ "introduction": {
+ "authors": [
+ "Yrahcaz7"
+ ]
+ }
+}
diff --git a/exercises/concept/poetry-club-door-policy/.approaches/introduction.md b/exercises/concept/poetry-club-door-policy/.approaches/introduction.md
new file mode 100644
index 0000000000..520930c6d1
--- /dev/null
+++ b/exercises/concept/poetry-club-door-policy/.approaches/introduction.md
@@ -0,0 +1,166 @@
+# Introduction
+
+There are various ways to solve each part of Poetry Club Door Policy.
+A commonality between most of the parts is needing to get a character from the provided string.
+
+There are multiple ways to do this, one of which is the standard way of using `[index]` access.
+
+One other way is to use [`charAt`][mdn-char-at], which is the same as `[index]` access for most purposes.
+
+Another method is [`at`][mdn-at], which is the same as `[index]` access, except it accepts negative numbers.
+A negative number will count backwards from the end of the string, unlike positive numbers, which count forwards from the start.
+
+In addition, [`substring`][mdn-substring] and [`slice`][mdn-slice] can be used.
+These string methods are normally used to get portions of strings, rather than a single character.
+
+An important distiction is that `slice` accepts negative numbers like `at` does, but `substring` does not.
+
+## Different ways to implement `frontDoorPassword`
+
+For `frontDoorPassword`, there are a variety of ways to make strings uppercase and lowercase.
+
+### Approach: `toUpperCase` and `toLowerCase`
+
+```js
+export function frontDoorPassword(word) {
+ return word[0].toUpperCase() + word.slice(1).toLowerCase();
+}
+```
+
+This approach is a standard method that uses [`toUpperCase`][mdn-to-upper-case] and [`toLowerCase`][mdn-to-lower-case].
+
+### Approach: `toLocaleUpperCase` and `toLocaleLowerCase`
+
+```js
+export function frontDoorPassword(word) {
+ return word[0].toLocaleUpperCase() + word.substring(1).toLocaleLowerCase();
+}
+```
+
+This approach uses [`toLocaleUpperCase`][mdn-to-locale-upper-case] and [`toLocaleLowerCase`][mdn-to-locale-lower-case], which are very similar to `toUpperCase` and `toLowerCase`, but work with either the current locale or a given locale, which can be specified as an argument.
+This approach is necessary when the language locale has a non-standard mapping between lower and uppercase.
+
+```javascript
+const str = 'istanbul';
+
+str.toUpperCase();
+// => 'ISTANBUL'
+str.toLocaleUpperCase('en-US');
+// => 'ISTANBUL'
+
+str.toLocaleUpperCase('tr');
+// => 'İSTANBUL'
+```
+
+### Approach: `String.fromCharCode` and `charCodeAt`
+
+```js
+export function frontDoorPassword(word) {
+ let charCode = word.charCodeAt(0);
+ if (charCode >= 97) {
+ charCode -= 32;
+ }
+
+ let password = String.fromCharCode(charCode);
+
+ for (let index = 1; index < word.length; index++) {
+ charCode = word.charCodeAt(index);
+ if (charCode <= 90) {
+ charCode += 32;
+ }
+
+ password += String.fromCharCode(charCode);
+ }
+ return password;
+}
+```
+
+This approach uses [`String.fromCharCode`][mdn-from-char-code] along with [`charCodeAt`][mdn-char-code-at].
+
+This method is much longer than the others and it only works with english letters, so it is less than ideal.
+
+## Different ways to implement `backDoorResponse`
+
+There are many ways to go about trimming whitespace for `backDoorResponse`.
+
+### Approach: `trim` and `[index]` access
+
+```js
+export function backDoorResponse(line) {
+ const trimmed = line.trim();
+ return trimmed[trimmed.length - 1];
+}
+```
+
+This standard approach uses `[index]` access and the built-in string method [`trim`][mdn-trim], which trims any leading and trailing whitespace from a string.
+
+### Approach: `trimEnd` and `at`
+
+```js
+export function backDoorResponse(line) {
+ return line.trimEnd().at(-1);
+}
+```
+
+This approach uses the string method [`trimEnd`][mdn-trim-end], which only trims trailing whitespace, unlike `trim`.
+
+It also uses `at` instead of `[index]` access make the solution shorter.
+
+### Approach: `replaceAll` and `charAt`
+
+```js
+export function backDoorResponse(line) {
+ const trimmed = line.replaceAll(' ', '');
+ return trimmed.charAt(trimmed.length - 1);
+}
+```
+
+This approach uses [`replaceAll`][mdn-replace-all] to remove all of the spaces in the string.
+
+This merges all the words in the string together, but that doesn't matter here as we only care about the last character and not the rest of the string.
+
+### Approach: `replace` and literal `RegExp`
+
+```js
+export function backDoorResponse(line) {
+ const trimmed = line.replace(/\s/g, '');
+ return trimmed[trimmed.length - 1];
+}
+```
+
+This approach uses [`replace`][mdn-replace] with a [regular expression literal][mdn-regular-expressions], achieving a similar result to the previous approach.
+
+The main difference is that the previous approach only removes spaces, while this approach can remove any type of whitespace.
+
+### Approach: `for` loop
+
+```js
+export function backDoorResponse(line) {
+ for (let index = line.length - 1; index >= 0; index--) {
+ if (line[index] != ' ') {
+ return line[index];
+ }
+ }
+ return '';
+}
+```
+
+This approach does not trim whitespace.
+Instead, it uses a [for loop][mdn-for] to return the first character that is not a space from the end of the string.
+
+[mdn-char-at]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charAt
+[mdn-at]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/at
+[mdn-substring]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substring
+[mdn-slice]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/slice
+[mdn-to-upper-case]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase
+[mdn-to-lower-case]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLowerCase
+[mdn-to-locale-upper-case]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLocaleUpperCase
+[mdn-to-locale-lower-case]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLocaleLowerCase
+[mdn-from-char-code]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/fromCharCode
+[mdn-char-code-at]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt
+[mdn-trim]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim
+[mdn-trim-end]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trimEnd
+[mdn-replace-all]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll
+[mdn-replace]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace
+[mdn-regular-expressions]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions
+[mdn-for]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for
diff --git a/exercises/concept/poetry-club-door-policy/.eslintrc b/exercises/concept/poetry-club-door-policy/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/concept/poetry-club-door-policy/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/concept/poetry-club-door-policy/.gitignore b/exercises/concept/poetry-club-door-policy/.gitignore
index bdb912f98a..0c88ff6ec3 100644
--- a/exercises/concept/poetry-club-door-policy/.gitignore
+++ b/exercises/concept/poetry-club-door-policy/.gitignore
@@ -1,3 +1,5 @@
-node_modules
-yarn-error.log
-
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/poetry-club-door-policy/.meta/exemplar.js b/exercises/concept/poetry-club-door-policy/.meta/exemplar.js
index 0dc048defb..7aaca7cf1c 100644
--- a/exercises/concept/poetry-club-door-policy/.meta/exemplar.js
+++ b/exercises/concept/poetry-club-door-policy/.meta/exemplar.js
@@ -30,8 +30,7 @@ export function frontDoorPassword(word) {
* @returns {string}
*/
export function backDoorResponse(line) {
- const trimmed = line.trim();
- return trimmed[trimmed.length - 1];
+ return line.trimEnd().slice(-1);
}
/**
@@ -42,5 +41,5 @@ export function backDoorResponse(line) {
* @returns {string} the back door password
*/
export function backDoorPassword(word) {
- return frontDoorPassword(word) + `, please`;
+ return frontDoorPassword(word) + ', please';
}
diff --git a/exercises/concept/poetry-club-door-policy/babel.config.js b/exercises/concept/poetry-club-door-policy/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/concept/poetry-club-door-policy/babel.config.js
+++ b/exercises/concept/poetry-club-door-policy/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/concept/poetry-club-door-policy/door-policy.spec.js b/exercises/concept/poetry-club-door-policy/door-policy.spec.js
index db1079ee5d..3847542712 100644
--- a/exercises/concept/poetry-club-door-policy/door-policy.spec.js
+++ b/exercises/concept/poetry-club-door-policy/door-policy.spec.js
@@ -1,10 +1,9 @@
-// @ts-check
-
+import { describe, expect, test } from '@jest/globals';
import {
- frontDoorResponse,
+ backDoorPassword,
backDoorResponse,
frontDoorPassword,
- backDoorPassword,
+ frontDoorResponse,
} from './door-policy';
const recite = (poem, responseFn) => {
@@ -61,12 +60,12 @@ describe('front door password', () => {
expect(frontDoorPassword('SUMMER')).toBe('Summer');
});
- test('should capitalize SOPHIA', () => {
- expect(frontDoorPassword('SOPHIA')).toBe('Sophia');
+ test('should capitalize sophia', () => {
+ expect(frontDoorPassword('sophia')).toBe('Sophia');
});
- test('should capitalize CODE', () => {
- expect(frontDoorPassword('CODE')).toBe('Code');
+ test('should capitalize Code', () => {
+ expect(frontDoorPassword('Code')).toBe('Code');
});
});
diff --git a/exercises/concept/poetry-club-door-policy/eslint.config.mjs b/exercises/concept/poetry-club-door-policy/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/poetry-club-door-policy/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/poetry-club-door-policy/jest.config.js b/exercises/concept/poetry-club-door-policy/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/poetry-club-door-policy/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/poetry-club-door-policy/package.json b/exercises/concept/poetry-club-door-policy/package.json
index 38e8fde78c..8502a5ecf7 100644
--- a/exercises/concept/poetry-club-door-policy/package.json
+++ b/exercises/concept/poetry-club-door-policy/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/concept/poetry-club-door-policy"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/concept/recycling-robot/.docs/hints.md b/exercises/concept/recycling-robot/.docs/hints.md
new file mode 100644
index 0000000000..58e1f325dc
--- /dev/null
+++ b/exercises/concept/recycling-robot/.docs/hints.md
@@ -0,0 +1,61 @@
+# Hints
+
+## 1. Check if a value is a boolean
+
+- You can use `typeof` to find the type of a value.
+- `typeof` returns a string.
+
+## 2. Check if a value is a number.
+
+- You can use `typeof` to find the type of a value.
+- `typeof` returns a string.
+- You need to check for `Infinity` and `NaN`.
+- `NaN` is never equal to itself, but there is a [built in function][isNaN] to check if a value is NaN.
+
+## 3. Check if a value is an object
+
+- You can use `typeof` to find the type of a value.
+- `typeof` returns a string.
+- You will need to check for `null`.
+
+## 4. Check if a string is numeric
+
+- You can use `typeof` to find the type of a value.
+- `typeof` returns a string.
+- You can iterate over a string to check if all characters are digits.
+
+## 5. Check if an object is electronic
+
+- You can use `instanceof` to check if an object is an instance of a class or one of its children.
+
+## 6. Check if a value is a non empty array
+
+- You can use `typeof` to find the type of a value.
+- `typeof` returns a string.
+- You can check the length of an array to find out how many elements it contains.
+
+## 7. Check if a value is an empty array
+
+- You can use `typeof` to find the type of a value.
+- `typeof` returns a string.
+- You can check the length of an array to find out how many elements it contains.
+
+## 8. Check if an object has a `type` property or method
+
+- You can use the `in` operator to check if an object has a property or method.
+
+## 9. Throw an error if an object does not have the `id` property or method
+
+- You can use the `in` operator to check if an object has a property or method.
+- If the `id` property or method is missing, your function should throw an `Error`.
+
+## 10. Check if an object has an `id` property
+
+- To check if an object has a property (not a method), you can use the `Object.hasOwn()` function.
+
+## 11. Check if an object has a defined `type` property
+
+- To check if an object has a property (not a method), you can use the `Object.hasOwn()` function.
+- You will have to access the `type` property and check if it is defined.
+
+[isNaN]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/isNaN
diff --git a/exercises/concept/recycling-robot/.docs/instructions.md b/exercises/concept/recycling-robot/.docs/instructions.md
new file mode 100644
index 0000000000..117a24171e
--- /dev/null
+++ b/exercises/concept/recycling-robot/.docs/instructions.md
@@ -0,0 +1,192 @@
+## Instructions
+
+You have been hired by a recycling center.
+Due to lack of space, all the products are put on the same conveyor belt, but this has lead to different materials mixing together, making them unusable.
+To fix this, you have been tasked with making functions to identify the type of a product.
+
+### 1. Check if a value is a boolean
+
+Implement the `isBoolean` function, that checks if a value is a boolean.
+
+```javascript
+isBoolean(true);
+// => true
+
+isBoolean(null);
+// => false
+```
+
+### 2. Check if a value is a number.
+
+Implement the `isNumber` function, that checks if a value is a _finite_ `number` or `bigint`, ie. not `NaN` or `Infinity`.
+
+Sometimes, the device for reading IDs fails and reads a non-numeric value as `NaN` (Not a Number) or `Infinity`.
+Your function should be able to correctly handle this as well.
+
+```javascript
+isNumber(42);
+// => true
+
+isNumber('Hello, World!');
+// => false
+
+isNumber(42n);
+// => true
+
+isNumber(NaN);
+// => false
+```
+
+### 3. Check if a value is an object
+
+Implement the `isObject` function, that should check if the value is an object.
+On the conveyor, `null` is nothing and not considered an object.
+
+```javascript
+isObject({ greeting: 'Hello' });
+// => true
+
+isObject(25n);
+// => false
+```
+
+### 4. Check if a string is numeric
+
+Implement the `isNumericString` function, that should check if the value is a string that only consists of digits or a minus followed by digits indicating a negative number.
+Only integers should be considered, decimals are not considered numeric for this check of the recycling robot.
+
+```javascript
+isNumericString(42);
+// => false
+
+isNumericString('42');
+// => true
+
+isNumericString('Hi!');
+// => false
+```
+
+### 5. Check if an object is electronic
+
+Implement the `isElectronic` function, that checks if an object is an instance of the provided `ElectronicDevice` class or one of its child classes.
+
+```javascript
+class Duck {
+ //...
+}
+
+class WashingMachine extends ElectronicDevice {
+ //...
+}
+
+isElectronic(new Duck());
+// => false
+
+isElectronic(new WashingMachine());
+// => true
+```
+
+### 6. Check if a value is a non empty array
+
+Implement the `isNonEmptyArray` function, that checks if a value is a non-empty array.
+
+```javascript
+isNonEmptyArray([1, 2, 3]);
+// => true
+
+isNonEmptyArray([]);
+// => false
+```
+
+### 7. Check if a value is an empty array
+
+Implement the `isEmptyArray` function, that checks if a value is an empty array.
+
+```javascript
+isEmptyArray([1, 2, 3]);
+// => false
+
+isEmptyArray([]);
+// => true
+```
+
+### 8. Check if an object has a `type` property or method
+
+Implement the `hasType` function, that checks whether an object has a `type` property or method.
+
+```javascript
+class Keyboard(){
+ type(){
+ // ...
+ }
+}
+hasType({ type:"car", color:"red" })
+// => true
+
+hasType({ color:"green" })
+// => false
+
+hasType(new Keyboard())
+// => true
+```
+
+### 9. Throw an error if an object does not have an `id` property or method
+
+Implement the `assertHasId` function, that will throw an `Error` if an object is missing the `id` property.
+
+If an object does have the `id` property, it should not return anything.
+
+```javascript
+assertHasId({ id: 42, color: 'red' });
+// => undefined
+
+assertHasId({ color: 'green' });
+// Error: "Object is missing the 'id' property"
+```
+
+### 10. Check if an object has an `id` property
+
+Implement the `hasIdProperty` function, that checks whether an object has an `id` property.
+
+```javascript
+class SimpleData {
+ constructor() {
+ this.number = '42';
+ this.id = 'BC269327FE1D9B95';
+ }
+}
+
+class StealingData extends SimpleData {}
+
+class MethodData {
+ constructor() {
+ this.number = '42';
+ this._id = 'BC269327FE1D9B95';
+ }
+
+ get id() {
+ return this._id;
+ }
+}
+
+hasIdProperty(new SimpleData());
+// => true
+
+hasIdProperty(new MethodData());
+// => false
+
+hasIdProperty(new StealingData());
+// => false
+```
+
+### 11. Check if an object has a defined `type` property
+
+Implement the `hasDefinedType` function, that checks if an object has a `type` property that is not `undefined`.
+
+```javascript
+hasDefinedType({ type: undefined, color: 'red' });
+// => false
+
+hasDefinedType({ type: 'car', color: 'green' });
+// => true
+```
diff --git a/exercises/concept/recycling-robot/.docs/introduction.md b/exercises/concept/recycling-robot/.docs/introduction.md
new file mode 100644
index 0000000000..8628305814
--- /dev/null
+++ b/exercises/concept/recycling-robot/.docs/introduction.md
@@ -0,0 +1,164 @@
+# Introduction
+
+Knowning what the type of a piece of data is, is often very important for code to run smoothly and without errors.
+
+Javascript has several ways to check the type of a value or object.
+
+```exercism/note
+Javascript's type checking mechanisms can be somewhat unreliable.
+
+For better type safety and stronger types, you should probably use TypeScript, a language that builds on JavaScript, but with the type syntax of a static-typed language.
+```
+
+## The `typeof` operator
+
+The `typeof` operator returns the type of its operand.
+The output is a string matching the name of one of the [primitive data types][primitives], except for `"null"`.
+It can also be `"function"` or `"object"`.
+
+```javascript
+typeof undefined;
+// => "undefined"
+
+typeof true;
+// => "boolean"
+
+typeof 42;
+// => "number"
+
+typeof 'Hello, World!';
+// => "string"
+
+typeof function () {
+ return 'Hello, World';
+};
+// => "function"
+
+typeof [1, 2, 3, 4];
+// => "object"
+
+typeof { city: 'Stockholm', country: 'Sweden' };
+// => "object"
+```
+
+For [historical reasons][`typeof null` is `"object"`].
+
+## The `instanceof` operator
+
+For checking the type of an object, you can use the `instanceof` operator.
+It evaluates into a `boolean` depending on whether the second operand is included in the first operands' [prototype chain][prototype chain].
+To clarify, `instanceof` will return whether the first operand is an instance of second operand or one of its child classes.
+`instanceof` only works on objects.
+
+```javascript
+class Beverage {
+ // ...
+}
+
+// The Coffee class is a child of the Beverage class.
+class Coffee extends Beverage {
+ // ...
+}
+
+const java = new Coffee();
+
+java instanceof Coffee;
+// => true
+
+java instanceof Beverage;
+// => true
+```
+
+````exercism/advanced
+The `Array` class has a method called `Array.isArray()` that checks if its argument is an array.
+
+While `instanceof Array` will not work with an array created in a different realm such as an `iframe` in a webpage, `Array.isArray()` will.
+
+This is because the Array class has a different constructor in each realm, and each `iframe` has its own realm, meaning that the function in the prototype chain will be different, causing `instanceof Array` to fail.
+`Array.isArray()` is capable of ignoring this, and should always be used when possible.
+
+It can also survive false positives where an object isn't actually an `Array`, and merely has `Array` in its prototype chain.
+
+```javascript
+({ __proto__: Array.prototype }) instanceof Array
+// => true
+
+Array.isArray({ __proto__: Array.prototype })
+// => false
+```
+
+````
+
+## The `in` operator
+
+The `in` operator returns whether the first operand is a property of the second operand.
+It does not check that the property has a defined value.
+A property set to `undefined` will still be detected by `in`.
+
+```javascript
+class Coffee {
+ constructor() {
+ this.temperature = 'hot';
+ this.isDarkMatter = undefined;
+ }
+
+ coolDown() {
+ this.temperature = 'warm';
+ }
+}
+
+const espresso = new Coffee();
+
+'temperature' in espresso;
+// => true
+
+'color' in espresso;
+// => false
+
+'isDarkMatter' in espresso;
+// => true
+```
+
+````exercism/note
+`in` will return `true` for inherited properties and methods.
+
+```javascript
+"coolDown" in espresso
+// => true
+
+"constructor" in espresso
+// => true
+```
+
+To avoid this, use `Object.hasOwn()` instead
+````
+
+## The `Object.hasOwn()` function
+
+The `Object.hasOwn()` method returns whether the specified object _owns the given property_ (it is not inherited or a method).
+
+```javascript
+class Coffee {
+ constructor() {
+ this.temperature = 'hot';
+ }
+
+ coolDown() {
+ this.temperature = 'warm';
+ }
+}
+const cappuccino = new Coffee();
+
+Object.hasOwn(cappucino, 'temperature');
+// => true
+
+Object.hasOwn(cappucino, 'constructor');
+// => false
+
+Object.hasOwn(cappucino, 'coolDown');
+// => false
+```
+
+[primitives]: https://developer.mozilla.org/en-US/docs/Glossary/Primitive
+[typeof null is object]: https://2ality.com/2013/10/typeof-null.html
+[prototype chain]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Inheritance_and_the_prototype_chain
diff --git a/exercises/concept/recycling-robot/.gitignore b/exercises/concept/recycling-robot/.gitignore
new file mode 100644
index 0000000000..0c88ff6ec3
--- /dev/null
+++ b/exercises/concept/recycling-robot/.gitignore
@@ -0,0 +1,5 @@
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/recycling-robot/.meta/config.json b/exercises/concept/recycling-robot/.meta/config.json
new file mode 100644
index 0000000000..aec5af47a5
--- /dev/null
+++ b/exercises/concept/recycling-robot/.meta/config.json
@@ -0,0 +1,21 @@
+{
+ "authors": [
+ "quintuple-mallard",
+ "SleeplessByte"
+ ],
+ "files": {
+ "solution": [
+ "assembly-line.js"
+ ],
+ "test": [
+ "assembly-line.spec.js"
+ ],
+ "exemplar": [
+ ".meta/exemplar.js"
+ ],
+ "editor": [
+ "lib.js"
+ ]
+ },
+ "blurb": "Learn about type checking while helping manage an assembly line"
+}
diff --git a/exercises/concept/recycling-robot/.meta/exemplar.js b/exercises/concept/recycling-robot/.meta/exemplar.js
new file mode 100644
index 0000000000..e2c4bf84f1
--- /dev/null
+++ b/exercises/concept/recycling-robot/.meta/exemplar.js
@@ -0,0 +1,123 @@
+// @ts-check
+//
+// The line above enables type checking for this file. Various IDEs interpret
+// the @ts-check directive. It will give you helpful autocompletion when
+// implementing this exercise.
+
+import { ElectronicDevice } from './lib.js';
+
+/**
+ * Checks if input is a boolean.
+ *
+ * @param {unknown} value
+ * @returns {value is boolean} whether the input is a boolean
+ */
+export function isBoolean(value) {
+ return typeof value === 'boolean';
+}
+
+/**
+ * Checks if input is a finite number or bigint.
+ *
+ * @param {unknown} value
+ * @returns {value is number | bigint} whether the input is a finite number or bigint
+ */
+export function isNumber(value) {
+ return (
+ (typeof value === 'number' && isFinite(value)) || typeof value === 'bigint'
+ );
+}
+
+/**
+ * Checks if a value is an object.
+ *
+ * @param {unknown} value
+ * @returns {value is object} whether the input is an object.
+ */
+export function isObject(value) {
+ return value !== null && typeof value === 'object';
+}
+
+/**
+ * Checks if a value is a numeric string.
+ *
+ * @param {unknown} value
+ * @returns {boolean} whether the input is a numeric string.
+ */
+export function isNumericString(value) {
+ return typeof value === 'string' && /^-?\d+$/.test(value);
+}
+
+/**
+ * Checks if an object is an instance of the "ElectronicDevice" class or one of its children.
+ *
+ * @param {object} object
+ * @returns {boolean} whether the object is an instance of the "ElectronicDevice" class or one of its children.
+ */
+export function isElectronic(object) {
+ return object instanceof ElectronicDevice;
+}
+
+/**
+ * Checks if a value is a non empty array.
+ *
+ * @param {unknown} value
+ * @returns {boolean} whether the input is a non empty array.
+ */
+export function isNonEmptyArray(value) {
+ return Array.isArray(value) && value.length > 0;
+}
+
+/**
+ * Checks if a value is an empty array.
+ *
+ * @param {unknown} value
+ * @returns {boolean} whether the input is an empty array.
+ */
+export function isEmptyArray(value) {
+ return Array.isArray(value) && value.length === 0;
+}
+
+/**
+ * Checks if a value has a "type" property or method.
+ *
+ * @param {object} object
+ * @returns {boolean} whether the input has a "type" property.
+ */
+export function hasType(object) {
+ return 'type' in object;
+}
+
+/**
+ * Throws an error if an object is missing an "id" property or method.
+ *
+ * @param {object} object
+ * @returns {never|void} undefined if the input has an "id" property, otherwise throws an error.
+ */
+export function assertHasId(object) {
+ if ('id' in object) {
+ return;
+ }
+
+ throw new Error('The "id" property is missing.');
+}
+
+/**
+ * Checks if a value has a "id" property.
+ *
+ * @param {object} object
+ * @returns {boolean} whether the input has a "id" property.
+ */
+export function hasIdProperty(object) {
+ return Object.hasOwn(object, 'id');
+}
+
+/**
+ * Checks if a value has a defined "type" property.
+ *
+ * @param {object} object
+ * @returns {boolean} whether the input has a defined "type" property.
+ */
+export function hasDefinedType(object) {
+ return Object.hasOwn(object, 'type') && object.type !== undefined;
+}
diff --git a/exercises/concept/recycling-robot/.npmrc b/exercises/concept/recycling-robot/.npmrc
new file mode 100644
index 0000000000..d26df800bb
--- /dev/null
+++ b/exercises/concept/recycling-robot/.npmrc
@@ -0,0 +1 @@
+audit=false
diff --git a/exercises/concept/recycling-robot/LICENSE b/exercises/concept/recycling-robot/LICENSE
new file mode 100644
index 0000000000..90e73be03b
--- /dev/null
+++ b/exercises/concept/recycling-robot/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Exercism
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/exercises/concept/recycling-robot/assembly-line.js b/exercises/concept/recycling-robot/assembly-line.js
new file mode 100644
index 0000000000..bdd32f2d47
--- /dev/null
+++ b/exercises/concept/recycling-robot/assembly-line.js
@@ -0,0 +1,121 @@
+// @ts-check
+//
+// The line above enables type checking for this file. Various IDEs interpret
+// the @ts-check directive. It will give you helpful autocompletion when
+// implementing this exercise.
+
+import { ElectronicDevice } from './lib.js';
+
+/**
+ * Checks if input is a boolean.
+ *
+ * @param {unknown} value
+ * @returns {boolean} whether the input is a boolean
+ */
+export function isBoolean(value) {
+ throw new Error('Remove this line and implement the isBoolean function');
+}
+
+/**
+ * Checks if input is a finite number or bigint.
+ *
+ * @param {unknown} value
+ * @returns {boolean} whether the input is a finite number or bigint
+ */
+export function isNumber(value) {
+ throw new Error('Remove this line and implement the isNumber function');
+}
+
+/**
+ * Checks if a value is an object.
+ *
+ * @param {unknown} value
+ * @returns {boolean} whether the input is an object.
+ */
+export function isObject(value) {
+ throw new Error('Remove this line and implement the isObject function');
+}
+
+/**
+ * Checks if a value is a numeric string.
+ *
+ * @param {unknown} value
+ * @returns {boolean} whether the input is a numeric string.
+ */
+export function isNumericString(value) {
+ throw new Error(
+ 'Remove this line and implement the isNumericString function',
+ );
+}
+
+/**
+ * Checks if an object is an instance of the `ElectronicDevice` class or one of its children.
+ *
+ * @param {object} object
+ * @returns {boolean} whether the object is an instance of the `ElectronicDevice` class or one of its children.
+ */
+export function isElectronic(object) {
+ throw new Error('Remove this line and implement the isElectronic function');
+}
+
+/**
+ * Checks if a value is a non empty array.
+ *
+ * @param {unknown} value
+ * @returns {boolean} whether the input is a non empty array.
+ */
+export function isNonEmptyArray(value) {
+ throw new Error(
+ 'Remove this line and implement the isNonEmptyArray function',
+ );
+}
+
+/**
+ * Checks if a value is an empty array.
+ *
+ * @param {unknown} value
+ * @returns {boolean} whether the input is an empty array.
+ */
+export function isEmptyArray(value) {
+ throw new Error('Remove this line and implement the isEmptyArray function');
+}
+
+/**
+ * Checks if a value has a "type" property or method.
+ *
+ * @param {object} object
+ * @returns {boolean} whether the input has a "type" property or method.
+ */
+export function hasType(object) {
+ throw new Error('Remove this line and implement the hasType function');
+}
+
+/**
+ * Throws an error if an object is missing an "id" property or method.
+ *
+ * @param {object} object
+ * @returns {never|void} undefined if the input has an "id" property or method, otherwise throws an error.
+ */
+export function assertHasId(object) {
+ throw new Error('Remove this line and implement the assertHasId function');
+}
+
+/**
+ * Checks if a value has an "id" property.
+ *
+ * @param {object} object
+ * @returns {boolean} whether the input has an "id" property.
+ */
+export function hasIdProperty(object) {
+ throw new Error('Remove this line and implement the hasIdProperty function');
+}
+
+/**
+ * Checks if a value has a defined "type" property.
+ *
+ * @param {object} object
+ * @returns {boolean} whether the input has a defined "type" property.
+ */
+export function hasDefinedType(object) {
+ throw new Error('Remove this line and implement the hasDefinedType function');
+}
diff --git a/exercises/concept/recycling-robot/assembly-line.spec.js b/exercises/concept/recycling-robot/assembly-line.spec.js
new file mode 100644
index 0000000000..d338348dff
--- /dev/null
+++ b/exercises/concept/recycling-robot/assembly-line.spec.js
@@ -0,0 +1,520 @@
+import { describe, expect, test } from '@jest/globals';
+import {
+ isBoolean,
+ isNumber,
+ isObject,
+ isNumericString,
+ isElectronic,
+ isNonEmptyArray,
+ isEmptyArray,
+ assertHasId,
+ hasType,
+ hasIdProperty,
+ hasDefinedType,
+} from './assembly-line';
+import { ElectronicDevice } from './lib.js';
+
+describe('isBoolean', () => {
+ test('returns true for true', () => {
+ expect(isBoolean(true)).toBe(true);
+ });
+
+ test('returns true for false', () => {
+ expect(isBoolean(false)).toBe(true);
+ });
+
+ test('returns false for numbers', () => {
+ expect(isBoolean(42)).toBe(false);
+ expect(isBoolean(42n)).toBe(false);
+ expect(isBoolean(0)).toBe(false);
+ });
+
+ test('returns false for strings', () => {
+ expect(isBoolean('Hello, World!')).toBe(false);
+ expect(isBoolean('42')).toBe(false);
+ expect(isBoolean('true')).toBe(false);
+ expect(isBoolean('')).toBe(false);
+ });
+
+ test('returns false for null', () => {
+ expect(isBoolean(null)).toBe(false);
+ });
+
+ test('returns false for undefined', () => {
+ expect(isBoolean(undefined)).toBe(false);
+ });
+
+ test('returns false for symbols', () => {
+ expect(isBoolean(Symbol('1'))).toBe(false);
+ expect(isBoolean(Symbol('true'))).toBe(false);
+ });
+
+ test('returns false for objects', () => {
+ expect(isBoolean({})).toBe(false);
+ expect(isBoolean({ true: false })).toBe(false);
+ });
+
+ test('returns false for arrays', () => {
+ expect(isBoolean([])).toBe(false);
+ expect(isBoolean([true, false])).toBe(false);
+ });
+});
+
+describe('isNumber', () => {
+ test('returns true for numbers', () => {
+ expect(isNumber(42)).toBe(true);
+ expect(isNumber(0)).toBe(true);
+ expect(isNumber(43_859_435.12)).toBe(true);
+ expect(isNumber(Number.MAX_SAFE_INTEGER)).toBe(true);
+ expect(isNumber(Number.MAX_VALUE)).toBe(true);
+ expect(isNumber(Number.MIN_SAFE_INTEGER)).toBe(true);
+ expect(isNumber(Number.MIN_VALUE)).toBe(true);
+ });
+
+ test('returns true for bigints', () => {
+ expect(isNumber(42n)).toBe(true);
+ expect(isNumber(0n)).toBe(true);
+ expect(isNumber(92n)).toBe(true);
+ expect(isNumber(1_848_958_451n)).toBe(true);
+ expect(isNumber(9_007_199_254_740_991n)).toBe(true);
+ expect(isNumber(9_999_999_999_999_999n)).toBe(true);
+ });
+
+ test('returns false for non-finite numbers such as NaN', () => {
+ expect(isNumber(NaN)).toBe(false);
+ expect(isNumber(Infinity)).toBe(false);
+ });
+
+ test('returns false for strings', () => {
+ expect(isNumber('Hello, World!')).toBe(false);
+ expect(isNumber('42')).toBe(false);
+ expect(isNumber('true')).toBe(false);
+ expect(isNumber('')).toBe(false);
+ });
+
+ test('returns false for null', () => {
+ expect(isNumber(null)).toBe(false);
+ });
+
+ test('returns false for undefined', () => {
+ expect(isNumber(undefined)).toBe(false);
+ });
+
+ test('returns false for symbols', () => {
+ expect(isNumber(Symbol('1'))).toBe(false);
+ expect(isNumber(Symbol('true'))).toBe(false);
+ });
+
+ test('returns false for objects', () => {
+ expect(isNumber({})).toBe(false);
+ expect(isNumber({ true: false })).toBe(false);
+ });
+
+ test('returns false for arrays', () => {
+ expect(isNumber([])).toBe(false);
+ expect(isNumber([1])).toBe(false);
+ });
+
+ test('returns false for booleans', () => {
+ expect(isNumber(true)).toBe(false);
+ expect(isNumber(false)).toBe(false);
+ });
+});
+
+class ClassForTesting {
+ constructor(number, word) {
+ this.number = number;
+ this.word = word;
+ }
+
+ id() {}
+}
+
+describe('isObject', () => {
+ test('returns true on object literals', () => {
+ expect(isObject({})).toBe(true);
+ expect(isObject({ greeting: 'hello' })).toBe(true);
+ });
+
+ test('returns true on class instances', () => {
+ expect(isObject(new ClassForTesting(5, 'Hello'))).toBe(true);
+ expect(isObject(new ClassForTesting(58, 'null'))).toBe(true);
+ expect(isObject(new ClassForTesting(1488, 'World!'))).toBe(true);
+ });
+
+ test('returns true for arrays which are objects', () => {
+ expect(isObject([])).toBe(true);
+ expect(isObject([{}])).toBe(true);
+ });
+
+ test('returns false on functions', () => {
+ expect(isObject(isObject)).toBe(false);
+ expect(isObject(() => {})).toBe(false);
+ expect(isObject(() => ({}))).toBe(false);
+ });
+
+ test('returns false for strings', () => {
+ expect(isObject('Hello, World!')).toBe(false);
+ expect(isObject('{}')).toBe(false);
+ expect(isObject('42')).toBe(false);
+ expect(isObject('true')).toBe(false);
+ expect(isObject('')).toBe(false);
+ });
+
+ test('returns false for null', () => {
+ expect(isObject(null)).toBe(false);
+ });
+
+ test('returns false for undefined', () => {
+ expect(isObject(undefined)).toBe(false);
+ });
+
+ test('returns false for symbols', () => {
+ expect(isObject(Symbol('1'))).toBe(false);
+ expect(isObject(Symbol('true'))).toBe(false);
+ });
+ test('returns false for booleans', () => {
+ expect(isObject(true)).toBe(false);
+ expect(isObject(false)).toBe(false);
+ });
+});
+
+describe('isNumericString', () => {
+ test('returns true on single-digit strings', () => {
+ expect(isNumericString('1')).toBe(true);
+ expect(isNumericString('0')).toBe(true);
+ expect(isNumericString('9')).toBe(true);
+ });
+
+ test('returns true on negative single-digit strings', () => {
+ expect(isNumericString('-1')).toBe(true);
+ expect(isNumericString('-0')).toBe(true);
+ expect(isNumericString('-9')).toBe(true);
+ });
+
+ test('returns true on multi-digit strings', () => {
+ expect(isNumericString('12')).toBe(true);
+ expect(isNumericString('00')).toBe(true);
+ expect(isNumericString('42')).toBe(true);
+ expect(isNumericString('-582')).toBe(true);
+ });
+
+ test('returns false on non-numeric strings', () => {
+ expect(isNumericString('')).toBe(false);
+ expect(isNumericString('-')).toBe(false);
+ expect(isNumericString('--')).toBe(false);
+ expect(isNumericString('--32')).toBe(false);
+ expect(isNumericString('Hello, World!')).toBe(false);
+ expect(isNumericString('')).toBe(false);
+ expect(isNumericString('NaN')).toBe(false);
+ });
+
+ test('returns false for bigint strings', () => {
+ expect(isNumericString('12n')).toBe(false);
+ expect(isNumericString('-582n')).toBe(false);
+ });
+
+ test('returns false for null', () => {
+ expect(isNumericString(null)).toBe(false);
+ });
+
+ test('returns false for undefined', () => {
+ expect(isNumericString(undefined)).toBe(false);
+ });
+
+ test('returns false for symbols', () => {
+ expect(isNumericString(Symbol('1'))).toBe(false);
+ expect(isNumericString(Symbol('true'))).toBe(false);
+ });
+
+ test('returns false for arrays', () => {
+ expect(isNumericString([])).toBe(false);
+ expect(isNumericString(['42'])).toBe(false);
+ });
+
+ test('returns false for booleans', () => {
+ expect(isNumericString(true)).toBe(false);
+ expect(isNumericString(false)).toBe(false);
+ });
+});
+
+class Oven extends ElectronicDevice {}
+class Computer extends ElectronicDevice {}
+class PersonalComputer extends Computer {}
+class HomeMadePersonalComputer extends PersonalComputer {}
+
+describe('isElectronic', () => {
+ test('returns true on ElectronicDevices', () => {
+ expect(isElectronic(new ElectronicDevice())).toBe(true);
+ });
+
+ test('returns true on sub-classes of ElectronicDevice', () => {
+ expect(isElectronic(new Oven())).toBe(true);
+ expect(isElectronic(new PersonalComputer())).toBe(true);
+ expect(isElectronic(new HomeMadePersonalComputer())).toBe(true);
+ });
+
+ test('returns false on electronic devices not created using the constructor', () => {
+ expect(isElectronic(Object.create(ElectronicDevice.prototype))).toBe(false);
+ expect(isElectronic({ __proto__: ElectronicDevice.prototype })).toBe(false);
+
+ const fakeDevice = {};
+ Object.setPrototypeOf(fakeDevice, ElectronicDevice.prototype);
+ expect(isElectronic(fakeDevice)).toBe(false);
+ });
+
+ test('returns false on non-electronic device objects', () => {
+ expect(isElectronic({ language: 'javascript', typing: 'dynamic' })).toBe(
+ false,
+ );
+ expect(isElectronic(new ClassForTesting(42, 'ElectronicDevice'))).toBe(
+ false,
+ );
+ expect(isElectronic([1, 2, 3, 4])).toBe(false);
+ });
+
+ test('returns false for strings', () => {
+ expect(isElectronic('12n')).toBe(false);
+ expect(isElectronic('ElectronicDevice')).toBe(false);
+ });
+
+ test('returns false for null', () => {
+ expect(isElectronic(null)).toBe(false);
+ });
+
+ test('returns false for undefined', () => {
+ expect(isElectronic(undefined)).toBe(false);
+ });
+
+ test('returns false for symbols', () => {
+ expect(isElectronic(Symbol('1'))).toBe(false);
+ expect(isElectronic(Symbol('true'))).toBe(false);
+ });
+
+ test('returns false for arrays', () => {
+ expect(isElectronic([])).toBe(false);
+ expect(isElectronic(['42'])).toBe(false);
+ });
+
+ test('returns false for booleans', () => {
+ expect(isElectronic(true)).toBe(false);
+ expect(isElectronic(false)).toBe(false);
+ });
+});
+
+describe('isNonEmptyArray', () => {
+ test('returns true for non-empty arrays', () => {
+ expect(isNonEmptyArray([1, 2, 3])).toBe(true);
+ expect(isNonEmptyArray(['a', 'b'])).toBe(true);
+
+ // The prototype of Array is also an array, but in Node it's considered empty
+ // expect(isNonEmptyArray(Array.prototype)).toBe(true);
+ });
+
+ test('returns false for empty arrays', () => {
+ expect(isNonEmptyArray([])).toBe(false);
+ });
+
+ test('returns false for fake non-empty arrays', () => {
+ expect(isNonEmptyArray({ __proto__: Array.prototype, length: 1 })).toBe(
+ false,
+ );
+
+ const fakeArray = { length: 1 };
+ Object.setPrototypeOf(fakeArray, Array.prototype);
+ expect(isNonEmptyArray(fakeArray)).toBe(false);
+ });
+
+ test('returns false for strings', () => {
+ expect(isNonEmptyArray('12n')).toBe(false);
+ expect(isNonEmptyArray('[1]')).toBe(false);
+ });
+
+ test('returns false for null', () => {
+ expect(isNonEmptyArray(null)).toBe(false);
+ });
+
+ test('returns false for undefined', () => {
+ expect(isNonEmptyArray(undefined)).toBe(false);
+ });
+
+ test('returns false for symbols', () => {
+ expect(isNonEmptyArray(Symbol('1'))).toBe(false);
+ expect(isNonEmptyArray(Symbol('[1]'))).toBe(false);
+ });
+
+ test('returns false for booleans', () => {
+ expect(isNonEmptyArray(true)).toBe(false);
+ expect(isNonEmptyArray(false)).toBe(false);
+ });
+});
+
+describe('isEmptyArray', () => {
+ test('returns true for empty arrays', () => {
+ expect(isEmptyArray([])).toBe(true);
+ });
+
+ test('returns false for non-empty arrays', () => {
+ expect(isEmptyArray([1, 2, 3])).toBe(false);
+ expect(isEmptyArray(['a', 'b'])).toBe(false);
+
+ // The prototype of Array is also an array, but in Node it's considered empty
+ // expect(isEmptyArray(Array.prototype)).toBe(false);
+ });
+
+ test('returns false on fake empty arrays', () => {
+ expect(isEmptyArray({ __proto__: Array.prototype, length: 0 })).toBe(false);
+ expect(isEmptyArray(Object.create(Array.prototype))).toBe(false);
+
+ const fakeArray = {};
+ Object.setPrototypeOf(fakeArray, Array.prototype);
+ expect(isNonEmptyArray(fakeArray)).toBe(false);
+ });
+
+ test('returns false for strings', () => {
+ expect(isEmptyArray('12n')).toBe(false);
+ expect(isEmptyArray('[]')).toBe(false);
+ });
+
+ test('returns false for null', () => {
+ expect(isEmptyArray(null)).toBe(false);
+ });
+
+ test('returns false for undefined', () => {
+ expect(isEmptyArray(undefined)).toBe(false);
+ });
+
+ test('returns false for symbols', () => {
+ expect(isEmptyArray(Symbol('1'))).toBe(false);
+ expect(isEmptyArray(Symbol('[]'))).toBe(false);
+ });
+
+ test('returns false for booleans', () => {
+ expect(isEmptyArray(true)).toBe(false);
+ expect(isEmptyArray(false)).toBe(false);
+ });
+});
+
+class MagicInspector {
+ type() {
+ return 'sleight of hand';
+ }
+}
+
+class MagicRevealer extends MagicInspector {
+ spill() {
+ throw new Error('A true magician never reveals their secrets');
+ }
+}
+
+describe('hasType', () => {
+ test('returns true if the type property exists', () => {
+ expect(hasType({ type: 'car', color: 'red' })).toBe(true);
+ });
+
+ test('returns true if the type method exists', () => {
+ expect(hasType(new MagicInspector())).toBe(true);
+ });
+
+ test('returns true if the type method is inherited', () => {
+ expect(hasType(new MagicRevealer())).toBe(true);
+ });
+
+ test('returns false if neither the type property, nor the method exists', () => {
+ expect(hasType({ color: 'green' })).toBe(false);
+ });
+});
+
+class IdGenerator {
+ id() {
+ return Math.random() * 42;
+ }
+}
+
+class MagicIdGenerator extends IdGenerator {
+ magic() {
+ return '🔮';
+ }
+}
+
+describe('assertHasId', () => {
+ test('returns nothing if the id property is present', () => {
+ expect(() => assertHasId({ id: 1 })).not.toThrow();
+ expect(() => assertHasId({ id: 42, color: 'red' })).not.toThrow();
+
+ const oven = new Oven();
+ oven.id = 42;
+ expect(() => assertHasId(oven)).not.toThrow();
+
+ // Even when there is no ID set
+ expect(() => assertHasId({ id: null })).not.toThrow();
+ });
+
+ test('returns nothing if the id method is present', () => {
+ expect(() => assertHasId(new IdGenerator())).not.toThrow();
+ });
+
+ test('returns nothing if the id method is inherited', () => {
+ expect(() => assertHasId(new MagicIdGenerator())).not.toThrow();
+ });
+
+ test("throws error if object has no 'id' property or method", () => {
+ expect(() => assertHasId({})).toThrow(Error);
+ expect(() => assertHasId({ color: 'green' })).toThrow(Error);
+ });
+});
+
+class SimpleData {
+ constructor() {
+ this.number = '42';
+ this.id = 'BC269327FE1D9B95';
+ }
+}
+
+class StealingData extends SimpleData {}
+
+class MethodData {
+ constructor() {
+ this.number = '42';
+ this._id = 'BC269327FE1D9B95';
+ }
+
+ get id() {
+ return this._id;
+ }
+}
+
+describe('hasIdProperty', () => {
+ test('returns true if it has the id property', () => {
+ expect(hasIdProperty({ id: 'test' })).toBe(true);
+ expect(hasIdProperty(new SimpleData())).toBe(true);
+ });
+
+ test('returns false if it does not have the id property', () => {
+ expect(hasIdProperty(new MethodData())).toBe(false);
+ expect(hasIdProperty({ color: 'green' })).toBe(false);
+ });
+
+ test('returns true if the id property was set in the constructor in the prototype chain', () => {
+ expect(hasIdProperty(new StealingData())).toBe(true);
+ });
+});
+
+describe('hasDefinedType', () => {
+ test('returns true if the type property is defined and set', () => {
+ expect(hasDefinedType({ type: 'car', color: 'green' })).toBe(true);
+ });
+
+ test('returns true if the type property is defined and set to an empty value', () => {
+ expect(hasDefinedType({ type: null, color: 'blue' })).toBe(true);
+ });
+
+ test('returns false if the type property is defined but not set', () => {
+ expect(hasDefinedType({ type: undefined, color: 'red' })).toBe(false);
+ });
+
+ test('returns false if the type property is missing', () => {
+ expect(hasDefinedType({ color: 'white' })).toBe(false);
+ expect(hasDefinedType(new MagicInspector())).toBe(false);
+ });
+});
diff --git a/exercises/concept/recycling-robot/babel.config.js b/exercises/concept/recycling-robot/babel.config.js
new file mode 100644
index 0000000000..a638497df1
--- /dev/null
+++ b/exercises/concept/recycling-robot/babel.config.js
@@ -0,0 +1,4 @@
+module.exports = {
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
+ plugins: [],
+};
diff --git a/exercises/concept/recycling-robot/eslint.config.mjs b/exercises/concept/recycling-robot/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/recycling-robot/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/recycling-robot/jest.config.js b/exercises/concept/recycling-robot/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/recycling-robot/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/recycling-robot/lib.js b/exercises/concept/recycling-robot/lib.js
new file mode 100644
index 0000000000..55ce7cadea
--- /dev/null
+++ b/exercises/concept/recycling-robot/lib.js
@@ -0,0 +1,18 @@
+const certification = Symbol('Certification');
+
+export class ElectronicDevice {
+ // This class will be used in the exercise.
+
+ static [Symbol.hasInstance](instance) {
+ return instance && instance.__certification === certification;
+ }
+
+ constructor() {
+ Object.defineProperty(this, '__certification', {
+ enumerable: false,
+ writable: false,
+ configurable: false,
+ value: certification,
+ });
+ }
+}
diff --git a/exercises/concept/recycling-robot/package.json b/exercises/concept/recycling-robot/package.json
new file mode 100644
index 0000000000..415c126f30
--- /dev/null
+++ b/exercises/concept/recycling-robot/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "@exercism/javascript-recycling-robot",
+ "description": "Exercism concept exercise on type checking",
+ "author": "Katrina Owen",
+ "contributors": [
+ "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/concept/recycling-robot"
+ },
+ "devDependencies": {
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
+ },
+ "dependencies": {},
+ "scripts": {
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
+}
diff --git a/exercises/concept/regular-chatbot/.docs/hints.md b/exercises/concept/regular-chatbot/.docs/hints.md
new file mode 100644
index 0000000000..0baf7dcba7
--- /dev/null
+++ b/exercises/concept/regular-chatbot/.docs/hints.md
@@ -0,0 +1,49 @@
+# Hints
+
+## 1. Check Valid Command
+
+- Use the [test()][mdn-regex-test] method for returning a boolean.
+- Remember that we can use [flags][mdn-flags] at the end of the regular expression for additional features such as case insensitive.
+
+## 2. Remove Encrypted Emojis
+
+- In this case we must use the [RexExp][mdn-regular-expressions] constructor to build our regular expression.
+- Thanks to the common encryption of each emoji, we can use `[0-9]` to search for any digit after the `emoji`word.
+- The character `+` matches one or more consecutive items.
+- Use the [split()][mdn-regex-split] method alongside the regex for each emoji to get rid of all encryption.
+- Finally, return this splitted array as a string using the [join()][mdn-join] method.
+
+## 3. Check Valid Phone Number
+
+- In this exercise we are playing with `groups and ranges`. Read this [documentation][mdn-groups] for learning more about it.
+- This [article][phone-validation] is really good at explaining different ways of number validation.
+- You may also want to use this [cheatsheet][mdn-regex-cheatsheet] for a broad view of available features in regular expressions.
+- Use the `test()` method to check whether the phone number is valid or not.
+- Return the final answer with a simple `if statement`.
+
+## 4. Get Website Link
+
+- We are targeting words that are joined by one or more `.(dots)`.
+- Remember to put the flag `g` at the end so we can get all the domains in any sentence.
+- Use the [match()][mdn-regex-match] method to return an array with all the previous matches.
+
+## 5. Greet the user
+
+- First, use the [replace()][mdn-regex-replace] method directly to the given string.
+- Use [Named Capture Groups][mdn-groups] to store the `name` and `surname` into different values.
+- Pass a `(...match)` destructuring to the replacer function so you can get the name and surname into the object within the matched groups.
+- Use the `pop()` method to store the matched object with `name` and `surname`.
+- Finally, implement a `template String` so you can get the values from the regular expression object. See [this][mdn-capturing-groups] example for knowing about how match() stores objects.
+
+[mdn-regex-cheatsheet]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Cheatsheet
+[mdn-regular-expressions]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
+[mdn-common-functions]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#using_regular_expressions_in_javascript
+[mdn-flags]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#advanced_searching_with_flags
+[mdn-regex-test]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test
+[mdn-regex-match]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match
+[mdn-regex-replace]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace
+[mdn-regex-split]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split
+[mdn-join]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/join
+[mdn-groups]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Groups_and_Ranges
+[phone-validation]: https://www.w3resource.com/javascript/form/phone-no-validation.php
+[mdn-capturing-groups]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match#using_named_capturing_groups
diff --git a/exercises/concept/regular-chatbot/.docs/instructions.md b/exercises/concept/regular-chatbot/.docs/instructions.md
new file mode 100644
index 0000000000..0dd94a71bb
--- /dev/null
+++ b/exercises/concept/regular-chatbot/.docs/instructions.md
@@ -0,0 +1,95 @@
+# Instructions
+
+You have been hired as a Regular Expression Specialist in a company that is developing a Chatbot.
+
+It is in a very basic phase of development, hence your mission is to use Regular Expressions to improve the chatbot's ability to understand and generate natural language.
+
+## Check Valid Command
+
+Apart from being smart, the Chatbot is also a loyal assistant.
+
+To ask something to the chatbot, the user must say the word “**Chatbot**” in the first position of the command.
+
+It doesn't matter if the keyword is in UPPERCASE or lowercase. The important aspect here is the position of the word.
+
+Implement the function `isValidCommand()` that helps the Chatbot recognize when the user is giving a command.
+
+```javascript
+isValidCommand("Chatbot, play a song from the 80's.";
+// => True
+isValidCommand("Hey Chatbot, where is the closest pharmacy?";
+// => False
+isValidCommand("CHATBOT, do you have a solution for this challenge?";
+// => True
+```
+
+## Remove encrypted emojis
+
+The Chatbot has a difficult time understanding how humans use emojis to express their emotions.
+
+When the chatbot receives user messages, each emoji is represented as “_emoji_” followed by its _id_ number.
+
+Implement the `removeEmoji()` method to take a string and remove all the emoji’s encryption throughout the message.
+
+Lines not containing emoji’s text should be returned unmodified.
+
+Just remove the emoji string. Do not attempt to adjust the whitespace.
+
+For this particular challenge, use constructor syntax for creating the regular expression.
+
+```javascript
+removeEmoji("I love playing videogames emoji3465 it's one of my hobbies");
+// => "I love playing videogames it's one of my hobbies"
+```
+
+## Check Valid Phone Number
+
+Considering the download of chatbot features on a mobile app, the user is expected to write a phone number during the conversation.
+
+The problem is that the chatbot can only read and validate a number with a specific format.
+
+If the number is valid (matches the character sequence specified by the **regular expression**), the chatbot answers with a message thanking the user and confirming the number. If the number is invalid, the function informs the user that the phone number is not valid.
+
+The expected format is: (+##) ###-###-###
+
+```javascript
+checkPhoneNumber('(+34) 659-771-594');
+// => "Thanks! You can now download me to your phone."
+checkPhoneNumber('659-771-594');
+// => "Oops, it seems like I can't reach out to 659-771-594"
+```
+
+## Get website link
+
+The Chatbot is a really curious software. Even though he can search on the internet about a particular topic, he likes to ask users about cool websites or URL’s to go find relevant information.
+
+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.org](http://exercism.org)
+
+Implement the function `getURL()` which is able to return an array with just the link of each website. Writing a regex to recognize any URL would be very complicated, but the Chatbot only needs to read simple URLs. For this exercise, a URL is any `"."`-delimited sequence of words.
+
+```javascript
+getURL(
+ 'I learned a lot from exercism.org and developer.mozilla.org, but I found ts39.es very boring!',
+);
+// => ["exercism.org", "developer.mozilla.org", "ts39.es"];
+```
+
+## Greet the user
+
+For storing data from all the persons who have had a conversation with, the chatbot is able to get the Full Name from the user’s profile in this style: **“smith, john”**.
+
+In this way, we want our chatbot to be really polite and make a good impression.
+
+Write the function `niceToMeetYou()` that takes a string with the full name of the user, and returns the string **“Nice to meet you, John Smith”**
+
+For learning purposes, implement the function using a **replacement method** from Regular Expressions.
+
+```javascript
+let str = 'Smith, John';
+
+niceToMeetYou(str);
+// => "Nice to meet you, John Smith"
+```
diff --git a/exercises/concept/regular-chatbot/.docs/introduction.md b/exercises/concept/regular-chatbot/.docs/introduction.md
new file mode 100644
index 0000000000..9d4b09ac22
--- /dev/null
+++ b/exercises/concept/regular-chatbot/.docs/introduction.md
@@ -0,0 +1,171 @@
+# Introduction
+
+## Regular Expressions in JavaScript
+
+A [Regular Expression][mdn-regular-expressions] (or Regex) is a sequence of characters that we can use to target and manipulate certain elements in strings. Hence, we can:
+
+- **Search** for a text in a string
+- **Replace** substrings in a string
+- **Extract** information from a string
+
+> 💡 JavaScript's regex flavor is part of the ECMA-262 standard for the language. This means that you don't have to worry about browser-specific compatibility.
+
+## How to create Regular Expressions
+
+In JavaScript, a regular expressions is mostly written in the format `/pattern/modifiers`.
+
+We have two ways of creating a regular expression:
+
+1. **Regular Expression Literal**:
+
+```javascript
+const regex = /[a-z]/;
+```
+
+2. Constructor **RegExp**:
+
+```javascript
+const regex = new RegExp('[a-z]');
+```
+
+In both cases, JavaScript is creating an object out of the regex. It is recommended to use immutable patterns with the literal as default.
+
+The RegExp constructor can be used for cases where the regex will change or we don't know it yet (like an input).
+
+## 🏴☠️ Flags
+
+Regular expressions have optional superpowers called [`flags`][mdn-flags] that allow for additional features.
+
+Some of the widely used are:
+
+- `/g` - Global Search
+- `/i` - Case Insensitive
+- `/m` - Multiline Search
+
+Here is a simple example:
+
+```javascript
+const re = /home/gi;
+const str = 'Home, sweet home.';
+const myArray = str.match(re);
+console.log(myArray);
+
+=> // ["Home", "home"]
+```
+
+The `g` character allows us to parse all possible matches within a string. Without this feature, JavaScript would have extracted only the first `Home` match.
+
+The Case Insensitive flag `/i` enables us to be flexible when it comes to finding a pattern. This way it doesn't matter if what we are looking for is in `UPPERCASE` or `lowercase`.
+
+When using the `RegExp` constructor, the syntax of adding flags is different.
+
+```javascript
+let regex = /[a-z]/gi; // literal notation
+let regex = new RegExp('[a-z]', 'gi'); // constructor with string pattern as first argument
+let regex = new RegExp(/[a-z]/, 'gi'); // constructor with regular expression literal as first argument (Starting with ECMAScript 6)
+```
+
+## Most common Functions
+
+When regular expressions are combined with current build-in functions in JavaScript, we have some powerful ways of manipulating and getting data from strings.
+
+These are some of the most common functions used alongside regex.
+
+### Test
+
+The [test()][mdn-regex-test] method executes a search for a match between a regular expression and a specified string. Returns true or false.
+
+```javascript
+const str = 'It is difficult to test if you have a virus';
+const result = /virus$/.test(str);
+
+console.log(result);
+
+// => true
+```
+
+### Match
+
+With the [match()][mdn-regex-match] method, we get a useful array whose contents depend on the presence or absence of the found matches.
+
+In this way, we are able both to **search** and to **extract** information from any string. For example:
+
+```javascript
+const funnyQuote =
+ 'If you see someone crying, ask if it is because of their haircut.';
+const regex1 = /someone/;
+const regex2 = /happy/;
+
+funnyQuote.match(regex1);
+// => ["someone", index: 11, input: "If you see someone crying, ask if it is because of their haircut.", groups: undefined]
+
+funnyQuote.match(regex2);
+// => null
+```
+
+When the Global Search flag `/g` is present, instead of getting the only match alongside useful information such as the index or input, the method returns all of the occurances displayed in the array:
+
+```javascript
+const funnyQuote =
+ 'If you see someone crying, ask if it is because of their haircut.';
+
+const regex3 = /if/gi;
+
+funnyQuote.match(regex3);
+// => ["If", "if"];
+```
+
+### Replace
+
+The [replace()][mdn-regex-replace] method in JavaScript allows us to search for a value within a given string, and replacing it with a desired new value.
+
+```javascript
+string.replace(searchValue, newValue);
+```
+
+The pattern for searching the substitution can be a single string, or a regular expression.
+
+```javascript
+let string = 'I like dogs!';
+let result = string.replace('dogs', 'cats');
+
+let string = 'I would love to travel to Japan';
+let result = string.replace(/Japan/g, 'Hawaii');
+```
+
+Moreover, we can apply a function on the replacement position in order to make additional changes to each value.
+
+```javascript
+let text = 'Say hello to the chatbot.';
+let result = text.replace(/chatbot|hello/gi, function (word) {
+ return word.toUpperCase();
+});
+// => "Say HELLO to the CHATBOT"
+```
+
+### Split
+
+The [split()][mdn-regex-split] method in JavaScript represents a different way of using and manipulating strings with regular expressions.
+
+In this way, we will be using regex in order to divide a given string by recognizing a pattern, e.g. `str.split(/[,.\s]/)`. This pattern will be used as the `separator`.
+
+```javascript
+const str = 'hello,user.how are.you';
+
+const result = str.split(/[,.\s]/);
+
+console.log(result);
+// => ['hello', 'user', 'how', 'are', 'you']
+```
+
+[using-regular-expressions-in-javascript]: https://www.regular-expressions.info/javascript.html
+[mdn-regex-cheatsheet]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Cheatsheet
+[mdn-regular-expressions]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
+[mdn-common-functions]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#using_regular_expressions_in_javascript
+[mdn-flags]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#advanced_searching_with_flags
+[mdn-regex-test]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test
+[mdn-regex-match]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match
+[mdn-regex-replace]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace
+[mdn-regex-split]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split
+[demystifying-regular-expressions-with-javascript]: https://livecodestream.dev/post/demystifying-regular-expressions-with-javascript/
+[how-to-use-the-js-replace-method-on-a-string]: https://www.freecodecamp.org/news/javascript-regex-match-example-how-to-use-the-js-replace-method-on-a-string/
diff --git a/exercises/concept/regular-chatbot/.gitignore b/exercises/concept/regular-chatbot/.gitignore
new file mode 100644
index 0000000000..0c88ff6ec3
--- /dev/null
+++ b/exercises/concept/regular-chatbot/.gitignore
@@ -0,0 +1,5 @@
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/regular-chatbot/.meta/config.json b/exercises/concept/regular-chatbot/.meta/config.json
new file mode 100644
index 0000000000..01dda4e544
--- /dev/null
+++ b/exercises/concept/regular-chatbot/.meta/config.json
@@ -0,0 +1,20 @@
+{
+ "authors": [
+ "julendiaz"
+ ],
+ "contributors": [
+ "junedev"
+ ],
+ "files": {
+ "solution": [
+ "regular-chatbot.js"
+ ],
+ "test": [
+ "regular-chatbot.spec.js"
+ ],
+ "exemplar": [
+ ".meta/exemplar.js"
+ ]
+ },
+ "blurb": "Learn the basics of regular expressions with JavaScript by helping a chatbot understand humans."
+}
diff --git a/exercises/concept/regular-chatbot/.meta/design.md b/exercises/concept/regular-chatbot/.meta/design.md
new file mode 100644
index 0000000000..c7dd1d24a6
--- /dev/null
+++ b/exercises/concept/regular-chatbot/.meta/design.md
@@ -0,0 +1,27 @@
+# Design
+
+## Learning objectives
+
+- Learn about the flavour of JavaScript regarding Regular Expressions.
+- How to create a regular expression, with regular expression literal and RexExp
+- How to pass flags in both versions
+- How to use the most common regex related functions, with special focus on test and match.
+- Understanding the different parts of the match result, incl. capture results
+- Using the replace method alongside regular expressions
+- Being aware of performance implications
+
+## Out of scope
+
+This exercise is intended for an Introduction to the flavour of Regular Expressions in JavaScript. The main focus resides in understanding the syntax behind the creation and manipulation of regex.
+
+Explaining how to write regular expressions themselves is out of scope for the concept here but we should link to some good resource a student could read to learn about them from scratch. We don't do this as part of the concept because Exercism assumes the student is already fluent in another language and most languages include some form of regular expressions.
+
+## Concepts
+
+- `regular-expressions`
+
+## Prerequisites
+
+- `arrays` as this is the result of match and other common functions.
+- `classes` to understand the new `Regex` syntax.
+- `objects` to know that every regular expression is creating an object out of the search.
diff --git a/exercises/concept/regular-chatbot/.meta/exemplar.js b/exercises/concept/regular-chatbot/.meta/exemplar.js
new file mode 100644
index 0000000000..f16b665266
--- /dev/null
+++ b/exercises/concept/regular-chatbot/.meta/exemplar.js
@@ -0,0 +1,65 @@
+// @ts-check
+
+/**
+ * Given a certain command, help the chatbot recognize whether the command is valid or not.
+ *
+ * @param {string} command
+ * @returns {boolean} whether or not is the command valid
+ */
+
+export function isValidCommand(command) {
+ const regex = /^chatbot/i;
+ return regex.test(command);
+}
+
+/**
+ * Given a certain message, help the chatbot get rid of all the emoji's encryption throught the message.
+ *
+ * @param {string} message
+ * @returns {string} The message without the emojis encryption
+ */
+export function removeEmoji(message) {
+ const regex = new RegExp('emoji[0-9]+');
+ return message.split(regex).join('');
+}
+
+/**
+ * Given a certain phone number, help the chatbot recognize whether it is in the correct format.
+ *
+ * @param {string} number
+ * @returns {string} the Chatbot response to the phone Validation
+ */
+export function checkPhoneNumber(number) {
+ const regex =
+ /^\(\+?([0-9]{2})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{3})[-. ]?([0-9]{3})$/;
+ if (regex.test(number)) {
+ return 'Thanks! You can now download me to your phone.';
+ } else {
+ return `Oops, it seems like I can't reach out to ${number}`;
+ }
+}
+
+/**
+ * Given a certain response from the user, help the chatbot get only the URL
+ *
+ * @param {string} userInput
+ * @returns {string[] | null} all the possible URL's that the user may have answered
+ */
+export function getURL(userInput) {
+ const regex = /(\w+\.)+\w+/g;
+ return userInput.match(regex);
+}
+
+/**
+ * Greet the user using its full name data from the profile
+ *
+ * @param {string} fullName
+ * @returns {string} Greeting from the chatbot
+ */
+export function niceToMeetYou(fullName) {
+ return fullName.replace(/(?\w+), (?\w+)/, (...match) => {
+ let groups = match.pop();
+
+ return `Nice to meet you, ${groups.name} ${groups.surname}`;
+ });
+}
diff --git a/exercises/concept/regular-chatbot/.npmrc b/exercises/concept/regular-chatbot/.npmrc
new file mode 100644
index 0000000000..d26df800bb
--- /dev/null
+++ b/exercises/concept/regular-chatbot/.npmrc
@@ -0,0 +1 @@
+audit=false
diff --git a/exercises/concept/regular-chatbot/LICENSE b/exercises/concept/regular-chatbot/LICENSE
new file mode 100644
index 0000000000..90e73be03b
--- /dev/null
+++ b/exercises/concept/regular-chatbot/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Exercism
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/exercises/concept/regular-chatbot/babel.config.js b/exercises/concept/regular-chatbot/babel.config.js
new file mode 100644
index 0000000000..a638497df1
--- /dev/null
+++ b/exercises/concept/regular-chatbot/babel.config.js
@@ -0,0 +1,4 @@
+module.exports = {
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
+ plugins: [],
+};
diff --git a/exercises/concept/regular-chatbot/eslint.config.mjs b/exercises/concept/regular-chatbot/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/regular-chatbot/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/regular-chatbot/jest.config.js b/exercises/concept/regular-chatbot/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/regular-chatbot/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/regular-chatbot/package.json b/exercises/concept/regular-chatbot/package.json
new file mode 100644
index 0000000000..95cbfe6eba
--- /dev/null
+++ b/exercises/concept/regular-chatbot/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "@exercism/javascript-regular-chatbot",
+ "description": "Exercism concept exercise on basics",
+ "author": "Julen Diaz ",
+ "private": true,
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/exercism/javascript",
+ "directory": "exercises/concept/regular-chatbot"
+ },
+ "devDependencies": {
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
+ },
+ "dependencies": {},
+ "scripts": {
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
+}
diff --git a/exercises/concept/regular-chatbot/regular-chatbot.js b/exercises/concept/regular-chatbot/regular-chatbot.js
new file mode 100644
index 0000000000..9bfe2d554e
--- /dev/null
+++ b/exercises/concept/regular-chatbot/regular-chatbot.js
@@ -0,0 +1,52 @@
+// @ts-check
+
+/**
+ * Given a certain command, help the chatbot recognize whether the command is valid or not.
+ *
+ * @param {string} command
+ * @returns {boolean} whether or not is the command valid
+ */
+
+export function isValidCommand(command) {
+ throw new Error('Remove this line and implement the function');
+}
+
+/**
+ * Given a certain message, help the chatbot get rid of all the emoji's encryption through the message.
+ *
+ * @param {string} message
+ * @returns {string} The message without the emojis encryption
+ */
+export function removeEmoji(message) {
+ throw new Error('Remove this line and implement the function');
+}
+
+/**
+ * Given a certain phone number, help the chatbot recognize whether it is in the correct format.
+ *
+ * @param {string} number
+ * @returns {string} the Chatbot response to the phone Validation
+ */
+export function checkPhoneNumber(number) {
+ throw new Error('Remove this line and implement the function');
+}
+
+/**
+ * Given a certain response from the user, help the chatbot get only the URL.
+ *
+ * @param {string} userInput
+ * @returns {string[] | null} all the possible URL's that the user may have answered
+ */
+export function getURL(userInput) {
+ throw new Error('Remove this line and implement the function');
+}
+
+/**
+ * Greet the user using the full name data from the profile.
+ *
+ * @param {string} fullName
+ * @returns {string} Greeting from the chatbot
+ */
+export function niceToMeetYou(fullName) {
+ throw new Error('Remove this line and implement the function');
+}
diff --git a/exercises/concept/regular-chatbot/regular-chatbot.spec.js b/exercises/concept/regular-chatbot/regular-chatbot.spec.js
new file mode 100644
index 0000000000..4d409d72aa
--- /dev/null
+++ b/exercises/concept/regular-chatbot/regular-chatbot.spec.js
@@ -0,0 +1,88 @@
+import { describe, expect, test } from '@jest/globals';
+import {
+ checkPhoneNumber,
+ getURL,
+ isValidCommand,
+ niceToMeetYou,
+ removeEmoji,
+} from './regular-chatbot';
+
+describe('isValidCommand', () => {
+ test('recognizes wheter the command is at the first position', () => {
+ expect(isValidCommand('Chatbot, Do you understand this command?')).toBe(
+ true,
+ );
+ expect(
+ isValidCommand(
+ 'Hey Chatbot, please tell me what is the weather for tomorrow.',
+ ),
+ ).toBe(false);
+ });
+
+ test('does not care about UPPERCASE or lowercase', () => {
+ expect(isValidCommand('CHATBOT, Is it okey if I shout at you?')).toBe(true);
+ expect(
+ isValidCommand('chatbot, please tell me what is happening here.'),
+ ).toBe(true);
+ });
+});
+
+describe('removeEmoji', () => {
+ test('removes properly one single emoji encryption', () => {
+ const expected = 'What was your name? Sorry I forgot about it.';
+ expect(
+ removeEmoji('What was your name? emoji2134 Sorry I forgot about it.'),
+ ).toBe(expected);
+ });
+
+ test('removes all the emoji encryption', () => {
+ const expected = ' How about ordering ?';
+ expect(removeEmoji('emoji5321 How about ordering emoji8921 ?')).toBe(
+ expected,
+ );
+ });
+});
+
+describe('checkPhoneNumber', () => {
+ test('recognizes a phone number with the correct format', () => {
+ const expected = 'Thanks! You can now download me to your phone.';
+ expect(checkPhoneNumber('(+34) 643-876-459')).toBe(expected);
+ expect(checkPhoneNumber('(+49) 543-928-190')).toBe(expected);
+ });
+
+ test('informs the user that it is a wrong phone number format', () => {
+ expect(checkPhoneNumber('322-787-654')).toBe(
+ "Oops, it seems like I can't reach out to 322-787-654",
+ );
+ expect(checkPhoneNumber('4355-67-274')).toBe(
+ "Oops, it seems like I can't reach out to 4355-67-274",
+ );
+ });
+});
+
+describe('getURL', () => {
+ test('returns only the link of the website', () => {
+ expect(getURL('You can check more info on youtube.com')).toStrictEqual([
+ 'youtube.com',
+ ]);
+ expect(
+ getURL('There is a cool website called theodinproject.com to learn from'),
+ ).toStrictEqual(['theodinproject.com']);
+ });
+
+ test('returns an array with the multiple websites links', () => {
+ expect(getURL('That was from reddit.com and notion.so')).toStrictEqual([
+ 'reddit.com',
+ 'notion.so',
+ ]);
+ });
+});
+
+describe('niceToMeetYou', () => {
+ test('greets the user by its proper name', () => {
+ expect(niceToMeetYou('Sanz, Pablo')).toBe('Nice to meet you, Pablo Sanz');
+ expect(niceToMeetYou('Stephan, Sandro')).toBe(
+ 'Nice to meet you, Sandro Stephan',
+ );
+ });
+});
diff --git a/exercises/concept/train-driver/.docs/hints.md b/exercises/concept/train-driver/.docs/hints.md
new file mode 100644
index 0000000000..fc5145a939
--- /dev/null
+++ b/exercises/concept/train-driver/.docs/hints.md
@@ -0,0 +1,32 @@
+# Hints
+
+## General
+
+- To extract multiple arguments in the function parameters so can you pack them with the `...`.
+- To use rest and spread use the `...` operator.
+
+## 1. Create a list of all wagons
+
+- Multiple arguments in the function parameters can be packed with the [`...` (spread) syntax][spread-syntax]. operator.
+
+## 2. Move the first two elements to the end of the array
+
+- You can unpack a series of parameters using [a destructuring assignment (`...`)][destructuring-assignment].
+ This lets you extract the first two elements of an `array` while keeping the rest intact.
+- To add another `array` into an existing `array`, you can use the `...` operator to "spread" the `array`.
+
+## 3. Add missing values
+
+- Using unpacking with the rest operator(`...`), you can extract the first element of an `array` while keeping the rest intact.
+- To add another `array` into an existing `array`, you can use the `...` operator to "spread" the `array`.
+
+## 4. Extend routing information
+
+- To add another `object` into an existing `object`, you can use the `...` operator to "spread" the `object`.
+
+## 5. Separate arrival time from routing information
+
+- To extract a value from an `object` while keeping the rest intact, you can use the rest operator(`...`).
+
+[spread-syntax]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
+[destructuring-assignment]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment
diff --git a/exercises/concept/train-driver/.docs/instructions.md b/exercises/concept/train-driver/.docs/instructions.md
new file mode 100644
index 0000000000..d9d89dea21
--- /dev/null
+++ b/exercises/concept/train-driver/.docs/instructions.md
@@ -0,0 +1,101 @@
+# Instructions
+
+Your friend Linus is a train driver who drives cargo trains between cities.
+Although they are amazing at handling trains, they are not amazing at handling logistics or computers.
+They would like to enlist your programming help organizing train details and correcting mistakes in route data.
+
+```exercism/note
+To practice, use the rest or spread operator to solve each of the tasks below.
+```
+
+## 1. Create a list of all wagons
+
+Your friend has been keeping track of each wagon identifier (ID), but they are never sure how many wagons the system is going to have to process at any given time.
+It would be much easier for the rest of the logistics program to have this data packaged into a unified `array`.
+
+Implement a function `getListOfWagons` that accepts an arbitrary number of wagon IDs which are the IDs of each wagon.
+Each ID will be a positive integer.
+The function should then return the given IDs as a single `array`.
+
+```javascript
+getListOfWagons(1, 7, 12, 3, 14, 8, 5);
+// => [1, 7, 12, 3, 14, 8, 5]
+```
+
+## 2. Move the first two elements to the end of the array
+
+At this point, you are starting to get a feel for the data and how it's used in the logistics program.
+The ID system always assigns the locomotive an ID of **1**, with the remainder of the wagons in the train assigned a randomly chosen ID greater than **1**.
+
+Your friend had to connect two new wagons to the train and forgot to update the system!
+Now, the first two wagons in the train `array` have to be moved to the end, or everything will be out of order.
+
+Linus would be really grateful to you for fixing their mistakes.
+
+Implement a function `fixListOfWagons` that accepts an array of the id of each wagon.
+It `return`s an `array` where the 2 first elements repositioned to the end of the `array` so that the locomotive can be in the front.
+
+```javascript
+eachWagonsID = [2, 5, 1, 7, 4, 12, 6, 3, 13];
+fixListOfWagons(eachWagonsID);
+// => [1, 7, 4, 12, 6, 3, 13, 2, 5]
+```
+
+## 3. Add missing values
+
+Uh-oh. some wagons seem to have gone missing.
+
+Fortunately, your friend just found another `array` which appears to contain the missing wagon IDs, and would like you to add them into the main wagon ID `array`.
+All they can remember is that the missing values should be placed directly after the designated locomotive.
+
+Given this new information, write a function called `correctListOfWagons` that takes two arrays which have the IDs of each wagon as the arguments.
+The wagon IDs of the second `array` should be added into the first `array` directly after the locomotive (ID 1).
+
+```javascript
+eachWagonsID = [1, 5, 20, 7, 4, 8];
+missingWagons = [3, 17, 6, 15];
+correctListOfWagons(eachWagonsID, missingWagons);
+// => [1, 3, 17, 6, 15, 5, 20, 7, 4, 8]
+```
+
+## 4. Extend routing information
+
+Now that all the wagon data is correct, your friend would like you to update the systems routing information.
+Initial routing information has been constructed as an `object`, and you friend would like you to update it with the additions provided.
+Every route requires slightly different information, so your friend would really prefer a generic solution.
+
+Implement a function `extendRouteInformation` that accepts two `objects`.
+The first `object` contains which cities the train route moves between.
+
+The second `object` contains other routing details such as train speed or length.
+The function should return a consolidated `object` with all routing information.
+
+```exercism/note
+The variable `moreRouteInformation` can contain different properties.
+```
+
+```javascript
+route = { from: 'Berlin', to: 'Hamburg' };
+moreRouteInformation = { length: '100', speed: '50' };
+extendRouteInformation(route, moreRouteInformation);
+// => {from: "Berlin", to: "Hamburg", length: "100", speed: "50"}
+```
+
+## 5. Separate arrival time from routing information
+
+Your friend has noticed that they don't need the arrival time in the routing information.
+Therefore your friend would like you to separate the arrival time from the routing information.
+
+Implement a function `separateTimeOfArrival` that accepts an object with the routing information.
+The function should return an array where the first element of the array is the arrival time and the second element is an object with the routing information without arrival time.
+
+```javascript
+routeInformation = {
+ from: 'Berlin',
+ to: 'Hamburg',
+ length: '100',
+ timeOfArrival: '10:10',
+};
+separateTimeOfArrival(routeInformation);
+// => ["10:10", {from: "Berlin", to: "Hamburg", length: "100"}]
+```
diff --git a/exercises/concept/train-driver/.docs/introduction.md b/exercises/concept/train-driver/.docs/introduction.md
new file mode 100644
index 0000000000..53dbcfa7be
--- /dev/null
+++ b/exercises/concept/train-driver/.docs/introduction.md
@@ -0,0 +1,88 @@
+# Introduction
+
+JavaScript has a built-in `...` operator that makes it easier to work with indefinite numbers of elements. Depending on the context, it's called either a _rest operator_ or _spread operator_.
+
+## Rest operator
+
+### Rest elements
+
+When `...` appears on the left-hand side of an assignment, those three dots are known as the `rest` operator. The three dots together with a variable name is called a rest element. It collects zero or more values, and stores them into a single array.
+
+```javascript
+const [a, b, ...everythingElse] = [0, 1, 1, 2, 3, 5, 8];
+a;
+// => 0
+b;
+// => 1
+everythingElse;
+// => [1, 2, 3, 5, 8]
+```
+
+Note that in JavaScript, unlike some other languages, a `rest` element cannot have a trailing comma. It _must_ be the last element in a destructuring assignment. The example below throws a `SyntaxError`:
+
+```javascript
+const [...items, last] = [2, 4, 8, 16]
+```
+
+### Rest properties
+
+Similarly to arrays, the rest operator can also be used to collect one or more object properties and store them in a single object.
+
+```javascript
+const { street, ...address } = {
+ street: 'Platz der Republik 1',
+ postalCode: '11011',
+ city: 'Berlin',
+};
+street;
+// => 'Platz der Republik 1'
+address;
+// => {postalCode: '11011', city: 'Berlin'}
+```
+
+## Rest parameters
+
+When `...` appears in a function definition next to its last argument, that parameter is called a _rest parameter_. It allows the function to accept an indefinite number of arguments as an array.
+
+```javascript
+function concat(...strings) {
+ return strings.join(' ');
+}
+concat('one');
+// => 'one'
+concat('one', 'two', 'three');
+// => 'one two three'
+```
+
+## Spread
+
+### Spread elements
+
+When `...` appears on the right-hand side of an assignment, it's known as the `spread` operator. It expands an array into a list of elements. Unlike the rest element, it can appear anywhere in an array literal expression, and there can be more than one.
+
+```javascript
+const oneToFive = [1, 2, 3, 4, 5];
+const oneToTen = [...oneToFive, 6, 7, 8, 9, 10];
+oneToTen;
+// => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+const woow = ['A', ...oneToFive, 'B', 'C', 'D', 'E', ...oneToFive, 42];
+woow;
+// => ["A", 1, 2, 3, 4, 5, "B", "C", "D", "E", 1, 2, 3, 4, 5, 42]
+```
+
+### Spread properties
+
+Similarly to arrays, the spread operator can also be used to copy properties from one object to another.
+
+```javascript
+let address = {
+ postalCode: '11011',
+ city: 'Berlin',
+};
+address = { ...address, country: 'Germany' };
+// => {
+// postalCode: '11011',
+// city: 'Berlin',
+// country: 'Germany',
+// }
+```
diff --git a/exercises/concept/train-driver/.gitignore b/exercises/concept/train-driver/.gitignore
new file mode 100644
index 0000000000..0c88ff6ec3
--- /dev/null
+++ b/exercises/concept/train-driver/.gitignore
@@ -0,0 +1,5 @@
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/train-driver/.meta/config.json b/exercises/concept/train-driver/.meta/config.json
new file mode 100644
index 0000000000..b771267146
--- /dev/null
+++ b/exercises/concept/train-driver/.meta/config.json
@@ -0,0 +1,27 @@
+{
+ "authors": [
+ "meatball133"
+ ],
+ "contributors": [
+ "bethanyg",
+ "junedev"
+ ],
+ "files": {
+ "solution": [
+ "train-driver.js"
+ ],
+ "test": [
+ "train-driver.spec.js"
+ ],
+ "exemplar": [
+ ".meta/exemplar.js"
+ ]
+ },
+ "blurb": "Professionalize using rest and spread operators.",
+ "custom": {
+ "version.tests.compatibility": "jest-27",
+ "flag.tests.task-per-describe": true,
+ "flag.tests.may-run-long": false,
+ "flag.tests.includes-optional": false
+ }
+}
diff --git a/exercises/concept/train-driver/.meta/design.md b/exercises/concept/train-driver/.meta/design.md
new file mode 100644
index 0000000000..3d9bd201fd
--- /dev/null
+++ b/exercises/concept/train-driver/.meta/design.md
@@ -0,0 +1,24 @@
+# Design
+
+## Learning objectives
+
+- Using spread to turn an array into a list of parameters
+- Using rest elements to turn a list of parameters into an array
+- Using spread to turn an extract value out of an object
+- Using spread to combine objects
+- Using rest to collect multiple parameters into an array
+
+## Out of scope
+
+- Default values
+
+## Concepts
+
+- `rest-and-spread`
+
+## Prerequisites
+
+- `arrays` are needed to understand array restructuring
+- `functions` are needed as basis for rest parameters
+- `objects` are needed for object spread etc.
+- `array-destructuring` are needed to understand rest elements
diff --git a/exercises/concept/train-driver/.meta/exemplar.js b/exercises/concept/train-driver/.meta/exemplar.js
new file mode 100644
index 0000000000..716e020729
--- /dev/null
+++ b/exercises/concept/train-driver/.meta/exemplar.js
@@ -0,0 +1,59 @@
+// @ts-check
+//
+// The line above enables type checking for this file. Various IDEs interpret
+// the @ts-check directive. It will give you helpful autocompletion when
+// implementing this exercise.
+
+/**
+ * Return each wagon's id in form of an array.
+ *
+ * @param {number[]} ids
+ * @returns {number[]} wagon ids
+ */
+export function getListOfWagons(...ids) {
+ return ids;
+}
+
+/**
+ * Reorder the array of wagons by moving the first 2 wagons to the end of the array.
+ *
+ * @param {Iterable} ids
+ * @returns {number[]} reordered list of wagons
+ */
+export function fixListOfWagons([first, second, ...rest]) {
+ return [...rest, first, second];
+}
+
+/**
+ * Fixes the array of wagons by inserting an array of wagons after the first element in eachWagonsID.
+ *
+ * @param {Iterable} ids
+ * @param {Iterable} missingWagons
+ * @returns {number[]} corrected list of wagons
+ */
+export function correctListOfWagons([first, ...rest], missingWagons) {
+ return [first, ...missingWagons, ...rest];
+}
+
+/**
+ * Extend route information by adding another object
+ *
+ * @param {Record} information
+ * @param {Record} additional
+ * @returns {Record} extended route information
+ */
+export function extendRouteInformation(information, additional) {
+ return { ...information, ...additional };
+}
+
+/**
+ * Separate arrival time from the route information object
+ *
+ * @param {Record} information
+ * @returns {[string, Record]} array with arrival time and object without arrival time
+ */
+export function separateTimeOfArrival(information) {
+ const { timeOfArrival, ...rest } = information;
+
+ return [timeOfArrival, rest];
+}
diff --git a/exercises/concept/train-driver/.npmrc b/exercises/concept/train-driver/.npmrc
new file mode 100644
index 0000000000..d26df800bb
--- /dev/null
+++ b/exercises/concept/train-driver/.npmrc
@@ -0,0 +1 @@
+audit=false
diff --git a/exercises/concept/train-driver/LICENSE b/exercises/concept/train-driver/LICENSE
new file mode 100644
index 0000000000..90e73be03b
--- /dev/null
+++ b/exercises/concept/train-driver/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Exercism
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/exercises/concept/train-driver/babel.config.js b/exercises/concept/train-driver/babel.config.js
new file mode 100644
index 0000000000..a638497df1
--- /dev/null
+++ b/exercises/concept/train-driver/babel.config.js
@@ -0,0 +1,4 @@
+module.exports = {
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
+ plugins: [],
+};
diff --git a/exercises/concept/train-driver/eslint.config.mjs b/exercises/concept/train-driver/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/train-driver/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/train-driver/jest.config.js b/exercises/concept/train-driver/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/train-driver/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/train-driver/package.json b/exercises/concept/train-driver/package.json
new file mode 100644
index 0000000000..259806cea8
--- /dev/null
+++ b/exercises/concept/train-driver/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "@exercism/javascript-train-driver",
+ "description": "Exercism concept exercise on rest and spread operators",
+ "author": "Meatball133",
+ "private": true,
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/exercism/javascript",
+ "directory": "exercises/concept/train-driver"
+ },
+ "devDependencies": {
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
+ },
+ "dependencies": {},
+ "scripts": {
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
+}
diff --git a/exercises/concept/train-driver/train-driver.js b/exercises/concept/train-driver/train-driver.js
new file mode 100644
index 0000000000..4c14baa8a3
--- /dev/null
+++ b/exercises/concept/train-driver/train-driver.js
@@ -0,0 +1,57 @@
+// @ts-check
+//
+// The line above enables type checking for this file. Various IDEs interpret
+// the @ts-check directive. It will give you helpful autocompletion when
+// implementing this exercise.
+
+/**
+ * Return each wagon's id in form of an array.
+ *
+ * @param {...number} ids
+ * @returns {number[]} wagon ids
+ */
+export function getListOfWagons(a, b, c, d, e, f, g, h, i, j, k, l, m, n) {
+ throw new Error('Remove this line and implement the function');
+}
+
+/**
+ * Reorder the array of wagons by moving the first 2 wagons to the end of the array.
+ *
+ * @param {Iterable} ids
+ * @returns {number[]} reordered list of wagons
+ */
+export function fixListOfWagons(ids) {
+ throw new Error('Remove this line and implement the function');
+}
+
+/**
+ * Fixes the array of wagons by inserting an array of wagons after the first element in eachWagonsID.
+ *
+ * @param {Iterable} ids
+ * @param {Iterable} missingWagons
+ * @returns {number[]} corrected list of wagons
+ */
+export function correctListOfWagons(ids, missingWagons) {
+ throw new Error('Remove this line and implement the function');
+}
+
+/**
+ * Extend route information by adding another object
+ *
+ * @param {Record} information
+ * @param {Record} additional
+ * @returns {Record} extended route information
+ */
+export function extendRouteInformation(information, additional) {
+ throw new Error('Remove this line and implement the function');
+}
+
+/**
+ * Separate arrival time from the route information object
+ *
+ * @param {Record} information
+ * @returns {[string, Record]} array with arrival time and object without arrival time
+ */
+export function separateTimeOfArrival(information) {
+ throw new Error('Remove this line and implement the function');
+}
diff --git a/exercises/concept/train-driver/train-driver.spec.js b/exercises/concept/train-driver/train-driver.spec.js
new file mode 100644
index 0000000000..87f7a321fb
--- /dev/null
+++ b/exercises/concept/train-driver/train-driver.spec.js
@@ -0,0 +1,251 @@
+import { describe, test, expect } from '@jest/globals';
+
+import {
+ getListOfWagons,
+ fixListOfWagons,
+ correctListOfWagons,
+ extendRouteInformation,
+ separateTimeOfArrival,
+} from './train-driver';
+
+const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom');
+const customLogSymbol = Symbol.for('exercism.javascript.util.log');
+
+// Follow the instructions in case you are stuck on "list.method is not a function"
+class LimitedArray {
+ constructor(values) {
+ this.values = values;
+ }
+
+ // Enables rest syntax and spread operator, as wel as for of, etc.
+ [Symbol.iterator]() {
+ return this.values[Symbol.iterator]();
+ }
+
+ // Log value in non-upgraded environments
+ toString() {
+ return this.values.toString();
+ }
+
+ // Overrides logging in node (ie. students working locally)
+ [customInspectSymbol](depth, inspectOptions, inspect) {
+ const inner = this.values[customInspectSymbol]
+ ? this.values[customInspectSymbol](depth, inspectOptions, inspect)
+ : this.values.toString();
+
+ return `List of (${inner})`;
+ }
+
+ // Overrides log overrides in web environment (ie. students working in editor)
+ [customLogSymbol](depth, inspectOptions, inspect) {
+ const inner = this.values[customLogSymbol]
+ ? this.values[customLogSymbol](depth, inspectOptions, inspect)
+ : this.values.toString();
+
+ return `List of (${inner})`;
+ }
+}
+
+function list(...values) {
+ return new LimitedArray(values);
+}
+
+function time(timeOfArrival, route) {
+ Object.defineProperty(route, 'timeOfArrival', {
+ configurable: false,
+ writable: false,
+ enumerable: true,
+ value: timeOfArrival,
+ });
+
+ return route;
+}
+
+describe('getListOfWagons', () => {
+ test('returns the correct array', () => {
+ expect(getListOfWagons(1, 5, 2, 7, 4)).toStrictEqual([1, 5, 2, 7, 4]);
+ });
+
+ test('works for a few arguments', () => {
+ expect(getListOfWagons(1, 5)).toStrictEqual([1, 5]);
+ });
+
+ test('works for a one argument', () => {
+ expect(getListOfWagons(1)).toStrictEqual([1]);
+ });
+
+ test('works for many arguments', () => {
+ expect(getListOfWagons(1, 5, 6, 3, 9, 8, 4, 14, 24, 7)).toStrictEqual([
+ 1, 5, 6, 3, 9, 8, 4, 14, 24, 7,
+ ]);
+ });
+});
+
+describe('fixListOfWagons', () => {
+ test('reorders the first 2 wagons to the end of the array', () => {
+ const eachWagonsID = list(3, 7, 1, 14, 10, 4, 12, 6, 23, 17, 13, 20, 8, 19);
+ const expected = [1, 14, 10, 4, 12, 6, 23, 17, 13, 20, 8, 19, 3, 7];
+
+ expect(fixListOfWagons(eachWagonsID)).toStrictEqual(expected);
+ });
+
+ test('works when only 3 wagons given', () => {
+ const eachWagonsID = list(4, 2, 1);
+
+ expect(fixListOfWagons(eachWagonsID)).toStrictEqual([1, 4, 2]);
+ });
+
+ test('works for a few wagons', () => {
+ const eachWagonsID = list(3, 4, 1, 5, 7, 9, 10);
+
+ expect(fixListOfWagons(eachWagonsID)).toStrictEqual([1, 5, 7, 9, 10, 3, 4]);
+ });
+});
+
+describe('correctListOfWagons', () => {
+ test('returns a wagon weight list with the inserted array of values', () => {
+ const eachWagonsID = list(1, 6, 11, 15, 13, 14, 17, 22, 2, 16, 19, 21);
+ const missingWagons = list(8, 10, 5, 9, 3, 7, 20);
+ const expected = [
+ 1, 8, 10, 5, 9, 3, 7, 20, 6, 11, 15, 13, 14, 17, 22, 2, 16, 19, 21,
+ ];
+
+ expect(correctListOfWagons(eachWagonsID, missingWagons)).toStrictEqual(
+ expected,
+ );
+ });
+
+ test('works for short arrays', () => {
+ const eachWagonsID = list(1, 7, 15, 24);
+ const missingWagons = list(8, 6, 4);
+ const expected = [1, 8, 6, 4, 7, 15, 24];
+
+ expect(correctListOfWagons(eachWagonsID, missingWagons)).toStrictEqual(
+ expected,
+ );
+ });
+
+ test('works when missingWagons is longer', () => {
+ const eachWagonsID = list(1, 7, 15, 24);
+ const missingWagons = list(8, 6, 4, 5, 9, 21, 2, 13);
+ const expected = [1, 8, 6, 4, 5, 9, 21, 2, 13, 7, 15, 24];
+
+ expect(correctListOfWagons(eachWagonsID, missingWagons)).toStrictEqual(
+ expected,
+ );
+ });
+});
+
+describe('extendRouteInformation', () => {
+ test('correctly extends route information', () => {
+ const route = { from: 'Berlin', to: 'Hamburg' };
+ const moreRouteInformation = {
+ timeOfArrival: '12:00',
+ precipitation: '10',
+ temperature: '5',
+ };
+
+ const expected = {
+ from: 'Berlin',
+ to: 'Hamburg',
+ timeOfArrival: '12:00',
+ precipitation: '10',
+ temperature: '5',
+ };
+
+ expect(extendRouteInformation(route, moreRouteInformation)).toStrictEqual(
+ expected,
+ );
+ });
+
+ test('works when not adding precipitation', () => {
+ const route = { from: 'Paris', to: 'London' };
+ const moreRouteInformation = { timeOfArrival: '10:30', temperature: '20' };
+
+ const expected = {
+ from: 'Paris',
+ to: 'London',
+ timeOfArrival: '10:30',
+ temperature: '20',
+ };
+
+ expect(extendRouteInformation(route, moreRouteInformation)).toStrictEqual(
+ expected,
+ );
+ });
+
+ test('works when written in different order', () => {
+ const route = { from: 'Gothenburg', to: 'Copenhagen' };
+ const moreRouteInformation = {
+ precipitation: '1',
+ timeOfArrival: '21:20',
+ temperature: '-6',
+ };
+
+ const expected = {
+ from: 'Gothenburg',
+ to: 'Copenhagen',
+ precipitation: '1',
+ timeOfArrival: '21:20',
+ temperature: '-6',
+ };
+
+ expect(extendRouteInformation(route, moreRouteInformation)).toStrictEqual(
+ expected,
+ );
+ });
+});
+
+describe('separateTimeOfArrival', () => {
+ test('separates timeOfArrival from complete object', () => {
+ const route = time('12:00', {
+ from: 'Berlin',
+ to: 'Hamburg',
+ precipitation: '10',
+ temperature: '5',
+ });
+
+ const expected = [
+ '12:00',
+ { from: 'Berlin', to: 'Hamburg', precipitation: '10', temperature: '5' },
+ ];
+
+ expect(separateTimeOfArrival(route)).toStrictEqual(expected);
+ });
+
+ test('separates timeOfArrival with smaller object', () => {
+ const route = time('10:30', {
+ from: 'Paris',
+ to: 'London',
+ temperature: '20',
+ });
+
+ const expected = [
+ '10:30',
+ { from: 'Paris', to: 'London', temperature: '20' },
+ ];
+
+ expect(separateTimeOfArrival(route)).toStrictEqual(expected);
+ });
+
+ test('separates timeOfArrival from differently ordered object', () => {
+ const route = time('21:20', {
+ from: 'Gothenburg',
+ to: 'Copenhagen',
+ precipitation: '1',
+ temperature: '-6',
+ });
+
+ const expected = [
+ '21:20',
+ {
+ from: 'Gothenburg',
+ to: 'Copenhagen',
+ precipitation: '1',
+ temperature: '-6',
+ },
+ ];
+
+ expect(separateTimeOfArrival(route)).toStrictEqual(expected);
+ });
+});
diff --git a/exercises/concept/translation-service/.docs/instructions.md b/exercises/concept/translation-service/.docs/instructions.md
index 69a1021673..b1ed9b4dd1 100644
--- a/exercises/concept/translation-service/.docs/instructions.md
+++ b/exercises/concept/translation-service/.docs/instructions.md
@@ -37,20 +37,25 @@ On completion the `callback` function is called.
If that happens, it is okay to `api.request` again.
```javascript
-api.request('majQa’');
-// => Promise({ resolved: undefined })
+api.request('majQa’', callback);
+// => undefined
+//
+// later: the passed callback is called with undefined
+// because it was successful.
```
**⚠ Warning! ⚠**
-```exercism/caution
+
+~~~~exercism/caution
The API works its magic by teleporting in the various translators when a `request` comes in.
This is a very costly action, so it shouldn't be called when a translation *is* available.
Unfortunately, not everyone reads the manual, so there is a system in place to kick-out bad actors.
If an `api.request` is called for `text` is available, the API throws an `AbusiveClientError` for this call, **and every call after that**.
Ensure that you *never* request a translation if something has already been translated.
-```
+~~~~
+
## 1. Fetch a translation, ignoring the quality
@@ -93,7 +98,7 @@ service.batch([]);
## 3. Request a translation, retrying at most 2 times
Implement a premium user method `request(text)`, that _requests_ a translation be added to the _API storage_.
-The request should automatically retry if a failure occurs.
+The request should automatically retry if a failure occurs.
It should perform no more than **3 calls** for the same request (_don't upset the space translators!!!_).
- If `api.request` does not return an error, resolve with `undefined`
@@ -128,6 +133,8 @@ service.premium("'arlogh Qoylu'pu'?", 40);
**N.B.**
-```exercism/note
+
+~~~~exercism/note
The correct translation of `'arlogh Qoylu'pu'?` is **How many times has it been heard?**.
-```
+~~~~
+
diff --git a/exercises/concept/translation-service/.docs/introduction.md b/exercises/concept/translation-service/.docs/introduction.md
index b3ea8220ba..427a71173c 100644
--- a/exercises/concept/translation-service/.docs/introduction.md
+++ b/exercises/concept/translation-service/.docs/introduction.md
@@ -1,102 +1,271 @@
# Introduction
-The [`Promise`][promise-docs] object represents the eventual completion (or failure) of an
-asynchronous operation and its resulting value.
+The [`Promise`][promise-docs] object represents the eventual completion (or failure) of an asynchronous operation and its resulting value.
-The methods [`promise.then()`][promise-then], [`promise.catch()`][promise-catch], and [`promise.finally()`][promise-finally] are used to associate further action with a promise that becomes settled.
+
+~~~exercism/note
+This is a hard topic for many people, specially if you know programming in a language that is completely _synchronous_.
+If you feel overwhelmed, or you would like to learn more about **concurrency** and **parallelism**, [watch (via go.dev)][talk-blog] or [watch directly via vimeo][talk-video] and [read the slides][talk-slides] of the brilliant talk "Concurrency is not parallelism".
-For example:
+[talk-slides]: https://go.dev/talks/2012/waza.slide#1
+[talk-blog]: https://go.dev/blog/waza-talk
+[talk-video]: https://vimeo.com/49718712
+~~~
+
+## Lifecycle of a promise
+
+A `Promise` has three states:
+
+1. pending
+2. fulfilled
+3. rejected
+
+When it is created, a promise is pending.
+At some point in the future it may _resolve_ or _reject_.
+Once a promise is resolved or rejected once, it can never be resolved or rejected again, nor can its state change.
+
+In other words:
+
+1. When pending, a promise:
+ - may transition to either the fulfilled or rejected state.
+2. When fulfilled, a promise:
+ - must not transition to any other state.
+ - must have a value, which must not change.
+3. When rejected, a promise:
+ - must not transition to any other state.
+ - must have a reason, which must not change.
+
+## Resolving a promise
+
+A promise may be resolved in various ways:
```javascript
-const myPromise = new Promise(function (resolve, reject) {
- let sampleData = [2, 4, 6, 8];
- let randomNumber = Math.ceil(Math.random() * 5);
- if (sampleData[randomNumber]) {
- resolve(sampleData[randomNumber]);
- } else {
- reject('An error occured!');
- }
+// Creates a promise that is immediately resolved
+Promise.resolve(value);
+
+// Creates a promise that is immediately resolved
+new Promise((resolve) => {
+ resolve(value);
});
-myPromise
- .then(function (e) {
- console.log(e);
- })
- .catch(function (error) {
- throw new Error(error);
- })
- .finally(function () {
- console.log('Promise completed');
- });
+// Chaining a promise leads to a resolved promise
+somePromise.then(() => {
+ // ...
+ return value;
+});
+```
+
+In the examples above `value` can be _anything_, including an error, `undefined`, `null` or another promise.
+Usually you want to resolve with a value that's not an error.
+
+## Rejecting a promise
+
+A promise may be rejected in various ways:
+
+```javascript
+// Creates a promise that is immediately rejected
+Promise.reject(reason)
+
+// Creates a promise that is immediately rejected
+new Promise((_, reject) {
+ reject(reason)
+})
+
+// Chaining a promise with an error leads to a rejected promise
+somePromise.then(() => {
+ // ...
+ throw reason
+})
```
-## Methods
+In the examples above `reason` can be _anything_, including an error, `undefined` or `null`.
+Usually you want to reject with an error.
-These methods are available on `Promise.prototype`
+## Chaining a promise
-**then**
+A promise may be _continued_ with a future action once it resolves or rejects.
-> The `.then()` method takes up to two arguments; the first argument is a callback function for the resolved case of the promise, and the second argument is a callback function for the rejected case. Each `.then()` returns a newly generated promise object, which can optionally be used for chaining.[^1]
+- [`promise.then()`][promise-then] is called once `promise` resolves
+- [`promise.catch()`][promise-catch] is called once `promise` rejects
+- [`promise.finally()`][promise-finally] is called once `promise` either resolves or rejects
+
+### **then**
+
+Every promise is "thenable".
+That means that there is a function `then` available that will be executed once the original promise is resolves.
+Given `promise.then(onResolved)`, the callback `onResolved` receives the value the original promise was resolved with.
+This will always return a _new_ "chained" promise.
+
+Returning a `value` from `then` resolves the "chained" promise.
+Throwing a `reason` in `then` rejects the "chained" promise.
```javascript
const promise1 = new Promise(function (resolve, reject) {
- resolve('Success!');
+ setTimeout(() => {
+ resolve('Success!');
+ }, 1000);
});
-promise1.then(function (value) {
+const promise2 = promise1.then(function (value) {
console.log(value);
// expected output: "Success!"
+
+ return true;
});
```
-**catch**
+This will log `"Success!"` after approximately 1000 ms.
+The state & value of `promise1` will be `resolved` and `"Success!"`.
+The state & value of `promise2` will be `resolved` and `true`.
-> A `.catch()` is just a `.then()` without a slot for a callback function for the case when the promise is resolved. It is used to handle rejected promises.[^2]
+There is a second argument available that runs when the original promise rejects.
+Given `promise.then(onResolved, onRejected)`, the callback `onResolved` receives the value the original promise was resolved with, or the callback `onRejected` receives the reason the promise was rejected.
```javascript
-const promise1 = new Promise((resolve, reject) => {
- throw 'An error occured';
-});
+const promise1 = new Promise(function (resolve, reject) {
+ setTimeout(() => {
+ resolve('Success!');
+ }, 1000);
-promise1.catch(function (error) {
- console.error(error);
+ if (Math.random() < 0.5) {
+ reject('Nope!');
+ }
});
-// expected output: An error occured
+
+function log(value) {
+ console.log(value);
+ return true;
+}
+
+function shout(reason) {
+ console.error(reason.toUpperCase());
+ return false;
+}
+
+const promise2 = promise1.then(log, shout);
```
-**finally**
+- In about 1/2 of the cases, this will log `"Success!"` after approximately 1000 ms.
+ - The state & value of `promise1` will be `resolved` and `"Success!"`.
+ - The state & value of `promise2` will be `resolved` and `true`.
+- In about 1/2 of the cases, this will immediately log `"NOPE!"`.
+ - The state & value of `promise1` will be `rejected` and `Nope!`.
+ - The state & value of `promise2` will be `resolved` and `false`.
+
+It is important to understand that because of the rules of the lifecycle, when it `reject`s, the `resolve` that comes in ~1000ms later is silently ignored, as the internal state cannot change once it has rejected or resolved.
+It is important to understand that returning a value from a promise resolves it, and throwing a value rejects it.
+When `promise1` resolves and there is a chained `onResolved`: `then(onResolved)`, then that follow-up is a new promise that can resolve or reject.
+When `promise1` rejects but there is a chained `onRejected`: `then(, onRejected)`, then that follow-up is a new promise that can resolve or reject.
-> When the promise is settled, i.e either fulfilled or rejected, the specified callback function is executed. This provides a way for code to be run whether the promise was fulfilled successfully or rejected once the Promise has been dealt with.[^3]
+### **catch**
+
+Sometimes you want to capture errors and only continue when the original promise `reject`s.
+Given `promise.catch(onCatch)`, the callback `onCatch` receives the reason the original promise was rejected.
+This will always return a _new_ "chained" promise.
+
+Returning a `value` from `catch` resolves the "chained" promise.
+Throwing a `reason` in `catch` rejects the "chained" promise.
```javascript
-function findDataById(id) {
- return new Promise(function (resolve, reject) {
- let sampleData = [1, 2, 3, 4, 5];
- if (sampleData[id]) {
- resolve(sampleData[id]);
- } else {
- reject(new Error('Invalid id'));
- }
- });
+const promise1 = new Promise(function (resolve, reject) {
+ setTimeout(() => {
+ resolve('Success!');
+ }, 1000);
+
+ if (Math.random() < 0.5) {
+ reject('Nope!');
+ }
+});
+
+function log(value) {
+ console.log(value);
+ return 'done';
}
-findDataById(4)
- .then(function (response) {
- console.log(response);
+function recover(reason) {
+ console.error(reason.toUpperCase());
+ return 42;
+}
+
+const promise2 = promise1.catch(recover).then(log);
+```
+
+In about 1/2 of the cases, this will log `"Success!"` after approximately 1000 ms.
+In the other 1/2 of the cases, this will immediately log `42`.
+
+- If `promise1` resolves, `catch` is skipped and it reaches `then`, and logs the value.
+ - The state & value of `promise1` will be `resolved` and `"Success!"`.
+ - The state & value of `promise2` will be `resolved` and `"done"`;
+- If `promise1` rejects, `catch` is executed, which _returns a value_, and thus the chain is now `resolved`, and it reaches `then`, and logs the value.
+ - The state & value of `promise1` will be `rejected` and `"Nope!"`.
+ - The state & value of `promise2` will be `resolved` and `"done"`;
+
+### **finally**
+
+Sometimes you want to execute code after a promise settles, regardless if the promise resolves or rejects.
+Given `promise.finally(onSettled)`, the callback `onSettled` receives nothing.
+This will always return a _new_ "chained" promise.
+
+Returning a `value` from `finally` copies the status & value from the original promise, ignoring the `value`.
+Throwing a `reason` in `finally` rejects the "chained" promise, overwriting any status & value or reason from the original promise.
+
+## Example
+
+Various of the methods together:
+
+```javascript
+const myPromise = new Promise(function (resolve, reject) {
+ const sampleData = [2, 4, 6, 8];
+ const randomNumber = Math.round(Math.random() * 5);
+
+ if (sampleData[randomNumber]) {
+ resolve(sampleData[randomNumber]);
+ } else {
+ reject('Sampling did not result in a sample');
+ }
+});
+
+const finalPromise = myPromise
+ .then(function (sampled) {
+ // If the random number was 0, 1, 2, or 3, this will be
+ // reached and the number 2, 4, 6, or 8 will be logged.
+ console.log(`Sampled data: ${sampled}`);
+ return 'yay';
})
- .catch(function (err) {
- console.error(err);
+ .catch(function (reason) {
+ // If the random number was 4 or 5, this will be reached and
+ // reason will be "An error occurred". The entire chain will
+ // then reject with an Error with the reason as message.
+ throw new Error(reason);
})
.finally(function () {
+ // This will always log after either the sampled data is
+ // logged or the error is raised.
console.log('Promise completed');
});
```
----
+- In the cases `randomNumber` is `0-3`:
+ - `myPromise` will be resolved with the value `2, 4, 6, or 8`
+ - `finalPromise` will be resolved with the value `'yay'`
+ - There will be two logs:
+ - `Sampled data: ...`
+ - `Promise completed`
+- In the cases `randomNumber` is `4-5`:
+ - `myPromise` will be rejected with the reason `'Sampling did not result in a sample'`
+ - `finalPromise` will be rejected with the reason `Error('Sampling did not result in a sample')`
+ - There will be one log:
+ - `Promise completed`
+ - _in some environments_ this will yield an `"uncaught rejected promise: Error('Sampling did not result in a sample')"` log
+
+As shown above, `reject` works with a string, and a promise can also reject with an `Error`.
+
+
+~~~exercism/note
+If chaining promises or general usage is unclear, the [tutorial on MDN][mdn-promises] is a good resource to consume.
-[^1]: `then`, MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then
-[^2]: `catch`, MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch
-[^3]: `finally`, MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally
+[mdn-promises]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises
+~~~
[promise-docs]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
[promise-catch]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch
diff --git a/exercises/concept/translation-service/.eslintignore b/exercises/concept/translation-service/.eslintignore
deleted file mode 100644
index 925cb98e75..0000000000
--- a/exercises/concept/translation-service/.eslintignore
+++ /dev/null
@@ -1,2 +0,0 @@
-/global.d.ts
-/env.d.ts
diff --git a/exercises/concept/translation-service/.eslintrc b/exercises/concept/translation-service/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/concept/translation-service/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/concept/translation-service/.gitignore b/exercises/concept/translation-service/.gitignore
index bdb912f98a..0c88ff6ec3 100644
--- a/exercises/concept/translation-service/.gitignore
+++ b/exercises/concept/translation-service/.gitignore
@@ -1,3 +1,5 @@
-node_modules
-yarn-error.log
-
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/translation-service/.meta/config.json b/exercises/concept/translation-service/.meta/config.json
index 10932f1883..694c26f511 100644
--- a/exercises/concept/translation-service/.meta/config.json
+++ b/exercises/concept/translation-service/.meta/config.json
@@ -3,7 +3,8 @@
"SleeplessByte"
],
"contributors": [
- "AndrewLawendy"
+ "AndrewLawendy",
+ "themetar"
],
"files": {
"solution": [
diff --git a/exercises/concept/translation-service/.meta/exemplar.alternative.js b/exercises/concept/translation-service/.meta/exemplar.alternative.js
index 37816be861..1a184def5e 100644
--- a/exercises/concept/translation-service/.meta/exemplar.alternative.js
+++ b/exercises/concept/translation-service/.meta/exemplar.alternative.js
@@ -80,11 +80,13 @@ export class TranslationService {
*/
async request(text, attempt = 1) {
try {
- await new Promise((resolve, reject) => {
- this.api.request(text, (err) => {
- err ? reject(err) : resolve();
- });
- });
+ await /** @type {Promise} */ (
+ new Promise((resolve, reject) => {
+ this.api.request(text, (err) => {
+ err ? reject(err) : resolve();
+ });
+ })
+ );
} catch (err) {
if (attempt === 3) {
throw err;
@@ -100,7 +102,7 @@ export class QualityThresholdNotMet extends Error {
super(
`
The translation of ${text} does not meet the requested quality threshold.
- `.trim()
+ `.trim(),
);
}
}
@@ -110,7 +112,7 @@ export class BatchIsEmpty extends Error {
super(
`
Requested a batch translation, but there are no texts in the batch.
- `.trim()
+ `.trim(),
);
}
}
diff --git a/exercises/concept/translation-service/.meta/exemplar.js b/exercises/concept/translation-service/.meta/exemplar.js
index b8a372b28a..6219de5e24 100644
--- a/exercises/concept/translation-service/.meta/exemplar.js
+++ b/exercises/concept/translation-service/.meta/exemplar.js
@@ -96,7 +96,7 @@ export class QualityThresholdNotMet extends Error {
*/
constructor(text) {
super(
- `The translation of ${text} does not meet the requested quality threshold.`
+ `The translation of ${text} does not meet the requested quality threshold.`,
);
this.text = text;
@@ -110,7 +110,7 @@ export class QualityThresholdNotMet extends Error {
export class BatchIsEmpty extends Error {
constructor() {
super(
- `Requested a batch translation, but there are no texts in the batch.`
+ `Requested a batch translation, but there are no texts in the batch.`,
);
}
}
diff --git a/exercises/concept/translation-service/api.js b/exercises/concept/translation-service/api.js
index 26f642f3b9..a4023a23bc 100644
--- a/exercises/concept/translation-service/api.js
+++ b/exercises/concept/translation-service/api.js
@@ -1,4 +1,9 @@
-import { AbusiveClientError, NotAvailable, Untranslatable } from './errors';
+import {
+ AbusiveClientError,
+ NotAvailable,
+ Untranslatable,
+ ConnectionError,
+} from './errors';
const mutex = { current: false };
@@ -41,7 +46,7 @@ export class ExternalApi {
fetch(text) {
if (typeof text !== 'string') {
throw new BadRequest(
- `Expected text when calling fetch(text), actual ${typeof text}.`
+ `Expected text when calling fetch(text), actual ${typeof text}.`,
);
}
@@ -68,13 +73,13 @@ export class ExternalApi {
request(text, callback) {
if (typeof text !== 'string') {
throw new BadRequest(
- `Expected string text when calling request(text, callback), actual ${typeof text}.`
+ `Expected string text when calling request(text, callback), actual ${typeof text}.`,
);
}
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}.`,
);
}
@@ -85,13 +90,12 @@ export class ExternalApi {
}
if (this.values[text]) {
- this.values[text].shift();
+ setTimeout(() => {
+ this.values[text].shift();
- // If it's now available, yay, otherwise, nay
- setTimeout(
- () => callback(this.values[text][0] ? undefined : makeRandomError()),
- 1
- );
+ // If it's now available, yay, otherwise, nay
+ callback(this.values[text][0] ? undefined : makeRandomError());
+ }, 1);
return;
}
@@ -114,7 +118,7 @@ function rejectWithRandomDelay(value) {
}
function makeRandomError() {
- return new Error(`Error code ${Math.ceil(Math.random() * 10000)}`);
+ return new ConnectionError(`Error code ${Math.ceil(Math.random() * 10000)}`);
}
class BadRequest extends Error {
diff --git a/exercises/concept/translation-service/babel.config.js b/exercises/concept/translation-service/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/concept/translation-service/babel.config.js
+++ b/exercises/concept/translation-service/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/concept/translation-service/errors.js b/exercises/concept/translation-service/errors.js
index 7c98b921a4..a3ed001521 100644
--- a/exercises/concept/translation-service/errors.js
+++ b/exercises/concept/translation-service/errors.js
@@ -3,7 +3,7 @@ export class NotAvailable extends Error {
super(
`
The requested text "${text}" has not been translated yet.
- `.trim()
+ `.trim(),
);
}
}
@@ -15,7 +15,7 @@ export class AbusiveClientError extends Error {
Your client has been rejected because of abusive behaviour.
naDevvo’ yIghoS!
- `.trim()
+ `.trim(),
);
}
}
@@ -25,3 +25,9 @@ export class Untranslatable extends Error {
super('jIyajbe’');
}
}
+
+export class ConnectionError extends Error {
+ constructor(message) {
+ super(message);
+ }
+}
diff --git a/exercises/concept/translation-service/eslint.config.mjs b/exercises/concept/translation-service/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/translation-service/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/translation-service/global.d.ts b/exercises/concept/translation-service/global.d.ts
index 5276467907..a3dfdeef46 100644
--- a/exercises/concept/translation-service/global.d.ts
+++ b/exercises/concept/translation-service/global.d.ts
@@ -17,5 +17,5 @@ interface Translation {
type fetchTranslation = (text: string) => Promise;
type requestTranslation = (
text: string,
- callback: (err?: Error) => void
+ callback: (err?: Error) => void,
) => void;
diff --git a/exercises/concept/translation-service/jest.config.js b/exercises/concept/translation-service/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/translation-service/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/translation-service/package.json b/exercises/concept/translation-service/package.json
index a86980acf5..f77fa82a3a 100644
--- a/exercises/concept/translation-service/package.json
+++ b/exercises/concept/translation-service/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/concept/translation-service"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/concept/translation-service/service.js b/exercises/concept/translation-service/service.js
index d1ee3b1c79..3320abef61 100644
--- a/exercises/concept/translation-service/service.js
+++ b/exercises/concept/translation-service/service.js
@@ -84,7 +84,7 @@ export class QualityThresholdNotMet extends Error {
super(
`
The translation of ${text} does not meet the requested quality threshold.
- `.trim()
+ `.trim(),
);
this.text = text;
@@ -100,7 +100,7 @@ export class BatchIsEmpty extends Error {
super(
`
Requested a batch translation, but there are no texts in the batch.
- `.trim()
+ `.trim(),
);
}
}
diff --git a/exercises/concept/translation-service/service.spec.js b/exercises/concept/translation-service/service.spec.js
index 32a72e014e..0514cb2b75 100644
--- a/exercises/concept/translation-service/service.spec.js
+++ b/exercises/concept/translation-service/service.spec.js
@@ -1,13 +1,12 @@
-// @ts-check
-
+import { beforeEach, describe, expect, test } from '@jest/globals';
import {
- TranslationService,
- QualityThresholdNotMet,
BatchIsEmpty,
+ QualityThresholdNotMet,
+ TranslationService,
} from './service';
-import { NotAvailable, Untranslatable } from './errors';
import { ExternalApi } from './api';
+import { ConnectionError, NotAvailable, Untranslatable } from './errors';
describe('Free service', () => {
/** @type {TranslationService} */
@@ -134,7 +133,7 @@ describe('Request service', () => {
test('it requests at most three times (does not retry thrice or more)', async () => {
const actual = service.request('ghobe’');
- await expect(actual).rejects.toThrow(Error);
+ await expect(actual).rejects.toThrow(ConnectionError);
});
});
@@ -182,7 +181,7 @@ describe('Premium service', () => {
test('it requests at most three times (does not retry thrice or more)', async () => {
const actual = service.premium('ghobe’', 0);
- await expect(actual).rejects.toThrow(Error);
+ await expect(actual).rejects.toThrow(ConnectionError);
});
test('it recognizes sufficient quality', async () => {
diff --git a/exercises/concept/vehicle-purchase/.docs/hints.md b/exercises/concept/vehicle-purchase/.docs/hints.md
index 7aad9c2126..a57dd002bd 100644
--- a/exercises/concept/vehicle-purchase/.docs/hints.md
+++ b/exercises/concept/vehicle-purchase/.docs/hints.md
@@ -1,6 +1,6 @@
# Hints
-## 1. Determine if you will need a drivers licence
+## 1. Determine if you will need a drivers license
- Use the [strict equals operator][mdn-equality-operators] to check whether your input equals a certain string.
- Use one of the two [logical operators][mdn-logical-operators] you learned about in the boolean concept to combine the two requirements.
diff --git a/exercises/concept/vehicle-purchase/.docs/instructions.md b/exercises/concept/vehicle-purchase/.docs/instructions.md
index 769dd2d957..b0dd514a3e 100644
--- a/exercises/concept/vehicle-purchase/.docs/instructions.md
+++ b/exercises/concept/vehicle-purchase/.docs/instructions.md
@@ -2,9 +2,9 @@
In this exercise, you will write some code to help you prepare to buy a vehicle.
-You have three tasks, one to determine if you will need to get a licence, one to help you choose between two vehicles and one to estimate the acceptable price for a used vehicle.
+You have three tasks, one to determine if you will need to get a license, one to help you choose between two vehicles and one to estimate the acceptable price for a used vehicle.
-## 1. Determine if you will need a drivers licence
+## 1. Determine if you will need a drivers license
Some kinds of vehicles require a drivers license to operate them.
Assume only the kinds `'car'` and `'truck'` require a license, everything else can be operated without a license.
@@ -41,7 +41,7 @@ For a rough estimate, assume if the vehicle is less than 3 years old, it costs 8
If it is more than 10 years old, it costs 50%.
If the vehicle is at least 3 years old but not older than 10 years, it costs 70% of the original price.
-Implement the `calculateResellPrice(originalPrice, age)` function that applies this logic using `if`, `else if` and `else` (there are other ways but you want to practice).
+Implement the `calculateResellPrice(originalPrice, age)` function that applies this logic using `if`, `else if` and `else` (there are other ways if you want to practice).
It takes the original price and the age of the vehicle as arguments and returns the estimated price in the dealership.
```javascript
diff --git a/exercises/concept/vehicle-purchase/.eslintrc b/exercises/concept/vehicle-purchase/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/concept/vehicle-purchase/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/concept/vehicle-purchase/.gitignore b/exercises/concept/vehicle-purchase/.gitignore
index bdb912f98a..0c88ff6ec3 100644
--- a/exercises/concept/vehicle-purchase/.gitignore
+++ b/exercises/concept/vehicle-purchase/.gitignore
@@ -1,3 +1,5 @@
-node_modules
-yarn-error.log
-
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/vehicle-purchase/.meta/exemplar.js b/exercises/concept/vehicle-purchase/.meta/exemplar.js
index 00aaf1dbae..69b3abf8c2 100644
--- a/exercises/concept/vehicle-purchase/.meta/exemplar.js
+++ b/exercises/concept/vehicle-purchase/.meta/exemplar.js
@@ -5,7 +5,7 @@
// implementing this exercise.
/**
- * Determines whether or not you need a licence to operate a certain kind of vehicle.
+ * Determines whether or not you need a license to operate a certain kind of vehicle.
*
* @param {string} kind
* @returns {boolean} whether a license is required
@@ -39,7 +39,7 @@ export function chooseVehicle(option1, option2) {
*
* @param {number} originalPrice
* @param {number} age
- * @returns expected resell price in the dealership
+ * @returns {number} expected resell price in the dealership
*/
export function calculateResellPrice(originalPrice, age) {
let percentage;
diff --git a/exercises/concept/vehicle-purchase/babel.config.js b/exercises/concept/vehicle-purchase/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/concept/vehicle-purchase/babel.config.js
+++ b/exercises/concept/vehicle-purchase/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/concept/vehicle-purchase/eslint.config.mjs b/exercises/concept/vehicle-purchase/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/vehicle-purchase/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/vehicle-purchase/jest.config.js b/exercises/concept/vehicle-purchase/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/vehicle-purchase/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/vehicle-purchase/package.json b/exercises/concept/vehicle-purchase/package.json
index 6fab2e89fc..37f50442d3 100644
--- a/exercises/concept/vehicle-purchase/package.json
+++ b/exercises/concept/vehicle-purchase/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/concept/vehicle-purchase"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/concept/vehicle-purchase/vehicle-purchase.js b/exercises/concept/vehicle-purchase/vehicle-purchase.js
index 8f80a66efb..aa414d4d38 100644
--- a/exercises/concept/vehicle-purchase/vehicle-purchase.js
+++ b/exercises/concept/vehicle-purchase/vehicle-purchase.js
@@ -5,13 +5,13 @@
// implementing this exercise.
/**
- * Determines whether or not you need a licence to operate a certain kind of vehicle.
+ * Determines whether or not you need a license to operate a certain kind of vehicle.
*
* @param {string} kind
* @returns {boolean} whether a license is required
*/
export function needsLicense(kind) {
- throw new Error('Please implement the needsLicense function');
+ throw new Error('Remove this line and implement the function');
}
/**
@@ -23,7 +23,7 @@ export function needsLicense(kind) {
* @returns {string} a sentence of advice which option to choose
*/
export function chooseVehicle(option1, option2) {
- throw new Error('Please implement the chooseVehicle function');
+ throw new Error('Remove this line and implement the function');
}
/**
@@ -32,8 +32,8 @@ export function chooseVehicle(option1, option2) {
*
* @param {number} originalPrice
* @param {number} age
- * @returns expected resell price in the dealership
+ * @returns {number} expected resell price in the dealership
*/
export function calculateResellPrice(originalPrice, age) {
- throw new Error('Please implement the calculateResellPrice function');
+ throw new Error('Remove this line and implement the function');
}
diff --git a/exercises/concept/vehicle-purchase/vehicle-purchase.spec.js b/exercises/concept/vehicle-purchase/vehicle-purchase.spec.js
index 12c5cc1a59..e38b916e6c 100644
--- a/exercises/concept/vehicle-purchase/vehicle-purchase.spec.js
+++ b/exercises/concept/vehicle-purchase/vehicle-purchase.spec.js
@@ -1,7 +1,8 @@
+import { describe, expect, test } from '@jest/globals';
import {
- needsLicense,
- chooseVehicle,
calculateResellPrice,
+ chooseVehicle,
+ needsLicense,
} from './vehicle-purchase';
describe('needsLicense', () => {
@@ -31,19 +32,19 @@ describe('chooseVehicle', () => {
test('correctly recommends the first option', () => {
expect(chooseVehicle('Bugatti Veyron', 'Ford Pinto')).toBe(
- 'Bugatti Veyron' + rest
+ 'Bugatti Veyron' + rest,
);
expect(chooseVehicle('Chery EQ', 'Kia Niro Elektro')).toBe(
- 'Chery EQ' + rest
+ 'Chery EQ' + rest,
);
});
test('correctly recommends the second option', () => {
expect(chooseVehicle('Ford Pinto', 'Bugatti Veyron')).toBe(
- 'Bugatti Veyron' + rest
+ 'Bugatti Veyron' + rest,
);
expect(chooseVehicle('2020 Gazelle Medeo', '2018 Bergamont City')).toBe(
- '2018 Bergamont City' + rest
+ '2018 Bergamont City' + rest,
);
});
});
diff --git a/exercises/concept/windowing-system/.docs/instructions.md b/exercises/concept/windowing-system/.docs/instructions.md
index fdd81ff266..b0980785a9 100644
--- a/exercises/concept/windowing-system/.docs/instructions.md
+++ b/exercises/concept/windowing-system/.docs/instructions.md
@@ -4,26 +4,26 @@ In this exercise, you will be simulating a windowing based computer system.
You will create some windows that can be moved and resized.
The following image is representative of the values you will be working with below.
-```
+```text
<--------------------- screenSize.width --------------------->
- ^ ╔════════════════════════════════════════════════════════════╗
- | ║ ║
- | ║ position.x,_ ║
- | ║ position.y \ ║
- | ║ \<----- size.width -----> ║
- | ║ ^ *──────────────────────┐ ║
- | ║ | │ title │ ║
- | ║ | ├──────────────────────┤ ║
-screenSize.height ║ | │ │ ║
- | ║ size.height │ │ ║
- | ║ | │ contents │ ║
- | ║ | │ │ ║
- | ║ | │ │ ║
- | ║ v └──────────────────────┘ ║
- | ║ ║
- | ║ ║
- v ╚════════════════════════════════════════════════════════════╝
+ ^ ┌────────────────────────────────────────────────────────────┐
+ | │ │
+ | │ position.x, _ │
+ | │ position.y \ │
+ | │ \<----- size.width -----> │
+ | │ ^ *──────────────────────┐ │
+ | │ | │ title │ │
+ | │ | ├──────────────────────┤ │
+screenSize.height │ | │ │ │
+ | │ size.height │ │ │
+ | │ | │ contents │ │
+ | │ | │ │ │
+ | │ | │ │ │
+ | │ v └──────────────────────┘ │
+ | │ │
+ | │ │
+ v └────────────────────────────────────────────────────────────┘
```
📣 To practice your wide range of JavaScript skills, **try to solve tasks 1 and 2 with prototype syntax and the remaining tasks with class syntax**.
diff --git a/exercises/concept/windowing-system/.docs/introduction.md b/exercises/concept/windowing-system/.docs/introduction.md
index 1f312ef87e..1f612f79f3 100644
--- a/exercises/concept/windowing-system/.docs/introduction.md
+++ b/exercises/concept/windowing-system/.docs/introduction.md
@@ -2,12 +2,12 @@
JavaScript includes the capabilities for object-oriented programming ([OOP][wiki-oop]).
In OOP, you want to create objects (_instances_) from "templates" (_classes_) so that they include certain data and functionality.
-The data properties are called _fields_ in the OOP context, function properties are called _methods_.
+The data properties are called _fields_ in the OOP context, the function properties are called _methods_.
JavaScript did not have classes at all before they were added to the language specification in 2015 but allowed for object-oriented programming using prototype-based inheritance.
And even though a `class` keyword is available nowadays, JavaScript is still a _prototype-based_ language.
-To understand what it means to be a prototype-based language and how JavaScript works, we will go back to the time when there were no classes.
+To understand what it means to be a prototype-based language and how JavaScript actually works, we will go back to the time when there were no classes.
## Prototype Syntax
@@ -31,14 +31,16 @@ Every instance object includes a hidden, internal property referred to as `[[pro
It holds a reference to the value of the `prototype` key of the constructor function.
Yes, you read that correctly, a JavaScript function can have key/value pairs because it is also an object behind the scenes.
-```exercism/note
+
+~~~~exercism/note
To summarize:
- Constructors in JavaScript are regular functions.
- Constructing a new instance creates an object with a relation to its constructor called its _prototype_.
- Functions are objects (callable objects) and therefore they can have properties.
- The constructor's (function) `prototype` property will become the instance's _prototype_.
-```
+~~~~
+
### Instance Fields
@@ -117,11 +119,13 @@ The `[[prototype]]` property of `Car.prototype` (`myCar.[[prototype]].[[prototyp
It contains general methods that are available for all JavaScript objects, e.g. `toString()`.
In conclusion, you can call `myCar.toString()` and that method will exist because JavaScript searches for that method throughout the whole prototype chain.
-```exercism/caution
+
+~~~~exercism/caution
Note that the prototype chain is only travelled when retrieving a value.
Setting a property directly or deleting a property of an instance object only targets that specific instance.
This might not be what you would expect when you are used to a language with class-based inheritance.
-```
+~~~~
+
## Class Syntax
@@ -180,24 +184,24 @@ With the keywords `get` and `set` you can define functions that are executed whe
```javascript
class Car {
constructor() {
- this._milage = 0;
+ this._mileage = 0;
}
- get milage() {
- return this._milage;
+ get mileage() {
+ return this._mileage;
}
- set milage(value) {
- throw new Error(`Milage cannot be manipulated, ${value} is ignored.`);
+ set mileage(value) {
+ throw new Error(`Mileage cannot be manipulated, ${value} is ignored.`);
// Just an example, usually you would not provide a setter in this case.
}
}
const myCar = new Car();
-myCar.milage;
+myCar.mileage;
// => 0
-myCar.milage = 100;
-// => Error: Milage cannot be manipulated, 100 is ignored.
+myCar.mileage = 100;
+// => Error: Mileage cannot be manipulated, 100 is ignored.
```
---
diff --git a/exercises/concept/windowing-system/.eslintrc b/exercises/concept/windowing-system/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/concept/windowing-system/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/concept/windowing-system/.gitignore b/exercises/concept/windowing-system/.gitignore
index bdb912f98a..0c88ff6ec3 100644
--- a/exercises/concept/windowing-system/.gitignore
+++ b/exercises/concept/windowing-system/.gitignore
@@ -1,3 +1,5 @@
-node_modules
-yarn-error.log
-
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/concept/windowing-system/babel.config.js b/exercises/concept/windowing-system/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/concept/windowing-system/babel.config.js
+++ b/exercises/concept/windowing-system/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/concept/windowing-system/eslint.config.mjs b/exercises/concept/windowing-system/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/concept/windowing-system/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/concept/windowing-system/jest.config.js b/exercises/concept/windowing-system/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/concept/windowing-system/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/concept/windowing-system/package.json b/exercises/concept/windowing-system/package.json
index d58b394751..4ae5775d4c 100644
--- a/exercises/concept/windowing-system/package.json
+++ b/exercises/concept/windowing-system/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/concept/windowing-system"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/concept/windowing-system/windowing-system.spec.js b/exercises/concept/windowing-system/windowing-system.spec.js
index 7797a306c9..1798a924b3 100644
--- a/exercises/concept/windowing-system/windowing-system.spec.js
+++ b/exercises/concept/windowing-system/windowing-system.spec.js
@@ -1,7 +1,8 @@
+import { describe, expect, test } from '@jest/globals';
import {
- Size,
Position,
ProgramWindow,
+ Size,
changeWindow,
} from './windowing-system';
diff --git a/exercises/practice/accumulate/.eslintrc b/exercises/practice/accumulate/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/practice/accumulate/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/accumulate/.gitignore b/exercises/practice/accumulate/.gitignore
new file mode 100644
index 0000000000..0c88ff6ec3
--- /dev/null
+++ b/exercises/practice/accumulate/.gitignore
@@ -0,0 +1,5 @@
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/practice/accumulate/accumulate.js b/exercises/practice/accumulate/accumulate.js
index 55d1745771..a15eb3fa37 100644
--- a/exercises/practice/accumulate/accumulate.js
+++ b/exercises/practice/accumulate/accumulate.js
@@ -4,5 +4,5 @@
//
export const accumulate = () => {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
};
diff --git a/exercises/practice/accumulate/accumulate.spec.js b/exercises/practice/accumulate/accumulate.spec.js
index dc5e9f0da8..8db9a8fc3f 100644
--- a/exercises/practice/accumulate/accumulate.spec.js
+++ b/exercises/practice/accumulate/accumulate.spec.js
@@ -1,3 +1,4 @@
+import { describe, expect, test, xtest } from '@jest/globals';
import { accumulate } from './accumulate';
describe('accumulate()', () => {
@@ -22,14 +23,14 @@ describe('accumulate()', () => {
const accumulator = (word) => word.split('').reverse().join('');
const result = accumulate(
'the quick brown fox etc'.split(/\s/),
- accumulator
+ accumulator,
);
expect(result).toEqual(['eht', 'kciuq', 'nworb', 'xof', 'cte']);
});
xtest('accumulate recursively', () => {
const result = accumulate('a b c'.split(/\s/), (char) =>
- accumulate('1 2 3'.split(/\s/), (digit) => char + digit)
+ accumulate('1 2 3'.split(/\s/), (digit) => char + digit),
);
expect(result).toEqual([
diff --git a/exercises/practice/accumulate/babel.config.js b/exercises/practice/accumulate/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/practice/accumulate/babel.config.js
+++ b/exercises/practice/accumulate/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/practice/accumulate/eslint.config.mjs b/exercises/practice/accumulate/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/practice/accumulate/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/practice/accumulate/jest.config.js b/exercises/practice/accumulate/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/practice/accumulate/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/practice/accumulate/package.json b/exercises/practice/accumulate/package.json
index 50e808bf78..577a2b30c9 100644
--- a/exercises/practice/accumulate/package.json
+++ b/exercises/practice/accumulate/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/practice/accumulate"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/practice/acronym/.docs/instructions.md b/exercises/practice/acronym/.docs/instructions.md
index e0515b4d1d..133bd2cbb7 100644
--- a/exercises/practice/acronym/.docs/instructions.md
+++ b/exercises/practice/acronym/.docs/instructions.md
@@ -4,5 +4,14 @@ Convert a phrase to its acronym.
Techies love their TLA (Three Letter Acronyms)!
-Help generate some jargon by writing a program that converts a long name
-like Portable Network Graphics to its acronym (PNG).
+Help generate some jargon by writing a program that converts a long name like Portable Network Graphics to its acronym (PNG).
+
+Punctuation is handled as follows: hyphens are word separators (like whitespace); all other punctuation can be removed from the input.
+
+For example:
+
+| Input | Output |
+| ------------------------- | ------ |
+| As Soon As Possible | ASAP |
+| Liquid-crystal display | LCD |
+| Thank George It's Friday! | TGIF |
diff --git a/exercises/practice/acronym/.eslintrc b/exercises/practice/acronym/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/practice/acronym/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/acronym/.gitignore b/exercises/practice/acronym/.gitignore
new file mode 100644
index 0000000000..0c88ff6ec3
--- /dev/null
+++ b/exercises/practice/acronym/.gitignore
@@ -0,0 +1,5 @@
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/practice/acronym/.meta/config.json b/exercises/practice/acronym/.meta/config.json
index fbeab6ff82..2e3b101d78 100644
--- a/exercises/practice/acronym/.meta/config.json
+++ b/exercises/practice/acronym/.meta/config.json
@@ -22,7 +22,7 @@
".meta/proof.ci.js"
]
},
- "blurb": "Convert a long phrase to its acronym",
+ "blurb": "Convert a long phrase to its acronym.",
"source": "Julien Vanier",
"source_url": "https://github.com/monkbroc",
"custom": {
diff --git a/exercises/practice/acronym/acronym.js b/exercises/practice/acronym/acronym.js
index 45e9a0e57d..e42d87a751 100644
--- a/exercises/practice/acronym/acronym.js
+++ b/exercises/practice/acronym/acronym.js
@@ -4,5 +4,5 @@
//
export const parse = () => {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
};
diff --git a/exercises/practice/acronym/acronym.spec.js b/exercises/practice/acronym/acronym.spec.js
index a8cc6f9f45..59a0eb7fe6 100644
--- a/exercises/practice/acronym/acronym.spec.js
+++ b/exercises/practice/acronym/acronym.spec.js
@@ -1,3 +1,4 @@
+import { describe, expect, test, xtest } from '@jest/globals';
import { parse } from './acronym';
describe('Acronyms are produced from', () => {
@@ -30,8 +31,8 @@ describe('Acronyms are produced from', () => {
xtest('long phrases', () => {
expect(
parse(
- 'Rolling On The Floor Laughing So Hard That My Dogs Came Over And Licked Me'
- )
+ 'Rolling On The Floor Laughing So Hard That My Dogs Came Over And Licked Me',
+ ),
).toEqual('ROTFLSHTMDCOALM');
});
diff --git a/exercises/practice/acronym/babel.config.js b/exercises/practice/acronym/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/practice/acronym/babel.config.js
+++ b/exercises/practice/acronym/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/practice/acronym/eslint.config.mjs b/exercises/practice/acronym/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/practice/acronym/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/practice/acronym/jest.config.js b/exercises/practice/acronym/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/practice/acronym/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/practice/acronym/package.json b/exercises/practice/acronym/package.json
index d9e23cb141..3d6b89ed1f 100644
--- a/exercises/practice/acronym/package.json
+++ b/exercises/practice/acronym/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/practice/acronym"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/practice/affine-cipher/.docs/instructions.md b/exercises/practice/affine-cipher/.docs/instructions.md
index 408cf591bd..1603dbbce9 100644
--- a/exercises/practice/affine-cipher/.docs/instructions.md
+++ b/exercises/practice/affine-cipher/.docs/instructions.md
@@ -1,73 +1,74 @@
# Instructions
-Create an implementation of the affine cipher,
-an ancient encryption system created in the Middle East.
+Create an implementation of the affine cipher, an ancient encryption system created in the Middle East.
The affine cipher is a type of monoalphabetic substitution cipher.
-Each character is mapped to its numeric equivalent, encrypted with
-a mathematical function and then converted to the letter relating to
-its new numeric value. Although all monoalphabetic ciphers are weak,
-the affine cypher is much stronger than the atbash cipher,
-because it has many more keys.
+Each character is mapped to its numeric equivalent, encrypted with a mathematical function and then converted to the letter relating to its new numeric value.
+Although all monoalphabetic ciphers are weak, the affine cipher is much stronger than the Atbash cipher, because it has many more keys.
+
+[//]: # " monoalphabetic as spelled by Merriam-Webster, compare to polyalphabetic "
+
+## Encryption
The encryption function is:
-`E(x) = (ax + b) mod m`
+```text
+E(x) = (ai + b) mod m
+```
-- where `x` is the letter's index from 0 - length of alphabet - 1
-- `m` is the length of the alphabet. For the roman alphabet `m == 26`.
-- and `a` and `b` make the key
+Where:
-The decryption function is:
+- `i` is the letter's index from `0` to the length of the alphabet - 1.
+- `m` is the length of the alphabet.
+ For the Latin alphabet `m` is `26`.
+- `a` and `b` are integers which make up the encryption key.
-`D(y) = a^-1(y - b) mod m`
+Values `a` and `m` must be _coprime_ (or, _relatively prime_) for automatic decryption to succeed, i.e., they have number `1` as their only common factor (more information can be found in the [Wikipedia article about coprime integers][coprime-integers]).
+In case `a` is not coprime to `m`, your program should indicate that this is an error.
+Otherwise it should encrypt or decrypt with the provided key.
-- where `y` is the numeric value of an encrypted letter, ie. `y = E(x)`
-- it is important to note that `a^-1` is the modular multiplicative inverse
- of `a mod m`
-- the modular multiplicative inverse of `a` only exists if `a` and `m` are
- coprime.
+For the purpose of this exercise, digits are valid input but they are not encrypted.
+Spaces and punctuation characters are excluded.
+Ciphertext is written out in groups of fixed length separated by space, the traditional group size being `5` letters.
+This is to make it harder to guess encrypted text based on word boundaries.
-To find the MMI of `a`:
+## Decryption
+
+The decryption function is:
-`an mod m = 1`
+```text
+D(y) = (a^-1)(y - b) mod m
+```
-- where `n` is the modular multiplicative inverse of `a mod m`
+Where:
-More information regarding how to find a Modular Multiplicative Inverse
-and what it means can be found [here.](https://en.wikipedia.org/wiki/Modular_multiplicative_inverse)
+- `y` is the numeric value of an encrypted letter, i.e., `y = E(x)`
+- it is important to note that `a^-1` is the modular multiplicative inverse (MMI) of `a mod m`
+- the modular multiplicative inverse only exists if `a` and `m` are coprime.
-Because automatic decryption fails if `a` is not coprime to `m` your
-program should return status 1 and `"Error: a and m must be coprime."`
-if they are not. Otherwise it should encode or decode with the
-provided key.
+The MMI of `a` is `x` such that the remainder after dividing `ax` by `m` is `1`:
-The Caesar (shift) cipher is a simple affine cipher where `a` is 1 and
-`b` as the magnitude results in a static displacement of the letters.
-This is much less secure than a full implementation of the affine cipher.
+```text
+ax mod m = 1
+```
-Ciphertext is written out in groups of fixed length, the traditional group
-size being 5 letters, and punctuation is excluded. This is to make it
-harder to guess things based on word boundaries.
+More information regarding how to find a Modular Multiplicative Inverse and what it means can be found in the [related Wikipedia article][mmi].
## General Examples
-- Encoding `test` gives `ybty` with the key a=5 b=7
-- Decoding `ybty` gives `test` with the key a=5 b=7
-- Decoding `ybty` gives `lqul` with the wrong key a=11 b=7
-- Decoding `kqlfd jzvgy tpaet icdhm rtwly kqlon ubstx`
- - gives `thequickbrownfoxjumpsoverthelazydog` with the key a=19 b=13
-- Encoding `test` with the key a=18 b=13
- - gives `Error: a and m must be coprime.`
- - because a and m are not relatively prime
-
-## Examples of finding a Modular Multiplicative Inverse (MMI)
-
-- simple example:
- - `9 mod 26 = 9`
- - `9 * 3 mod 26 = 27 mod 26 = 1`
- - `3` is the MMI of `9 mod 26`
-- a more complicated example:
- - `15 mod 26 = 15`
- - `15 * 7 mod 26 = 105 mod 26 = 1`
- - `7` is the MMI of `15 mod 26`
+- Encrypting `"test"` gives `"ybty"` with the key `a = 5`, `b = 7`
+- Decrypting `"ybty"` gives `"test"` with the key `a = 5`, `b = 7`
+- Decrypting `"ybty"` gives `"lqul"` with the wrong key `a = 11`, `b = 7`
+- Decrypting `"kqlfd jzvgy tpaet icdhm rtwly kqlon ubstx"` gives `"thequickbrownfoxjumpsoverthelazydog"` with the key `a = 19`, `b = 13`
+- Encrypting `"test"` with the key `a = 18`, `b = 13` is an error because `18` and `26` are not coprime
+
+## Example of finding a Modular Multiplicative Inverse (MMI)
+
+Finding MMI for `a = 15`:
+
+- `(15 * x) mod 26 = 1`
+- `(15 * 7) mod 26 = 1`, ie. `105 mod 26 = 1`
+- `7` is the MMI of `15 mod 26`
+
+[mmi]: https://en.wikipedia.org/wiki/Modular_multiplicative_inverse
+[coprime-integers]: https://en.wikipedia.org/wiki/Coprime_integers
diff --git a/exercises/practice/affine-cipher/.eslintrc b/exercises/practice/affine-cipher/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/practice/affine-cipher/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/affine-cipher/.gitignore b/exercises/practice/affine-cipher/.gitignore
new file mode 100644
index 0000000000..0c88ff6ec3
--- /dev/null
+++ b/exercises/practice/affine-cipher/.gitignore
@@ -0,0 +1,5 @@
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/practice/affine-cipher/.meta/config.json b/exercises/practice/affine-cipher/.meta/config.json
index b20757b184..2e56724755 100644
--- a/exercises/practice/affine-cipher/.meta/config.json
+++ b/exercises/practice/affine-cipher/.meta/config.json
@@ -18,7 +18,7 @@
},
"blurb": "Create an implementation of the Affine cipher, an ancient encryption algorithm from the Middle East.",
"source": "Wikipedia",
- "source_url": "http://en.wikipedia.org/wiki/Affine_cipher",
+ "source_url": "https://en.wikipedia.org/wiki/Affine_cipher",
"custom": {
"version.tests.compatibility": "jest-27",
"flag.tests.task-per-describe": false,
diff --git a/exercises/practice/affine-cipher/.meta/proof.ci.js b/exercises/practice/affine-cipher/.meta/proof.ci.js
index ee1665b0ed..6b129951d2 100644
--- a/exercises/practice/affine-cipher/.meta/proof.ci.js
+++ b/exercises/practice/affine-cipher/.meta/proof.ci.js
@@ -24,7 +24,6 @@ const isNumber = (candidate) => {
const findMMI = (a) => {
let i = 1;
- // eslint-disable-next-line no-constant-condition
while (true) {
i++;
diff --git a/exercises/practice/affine-cipher/affine-cipher.js b/exercises/practice/affine-cipher/affine-cipher.js
index 545736f6ab..e2b1330623 100644
--- a/exercises/practice/affine-cipher/affine-cipher.js
+++ b/exercises/practice/affine-cipher/affine-cipher.js
@@ -1,7 +1,7 @@
export const encode = (phrase, key) => {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
};
export const decode = (phrase, key) => {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
};
diff --git a/exercises/practice/affine-cipher/affine-cipher.spec.js b/exercises/practice/affine-cipher/affine-cipher.spec.js
index 9156312c5d..507aad8de3 100644
--- a/exercises/practice/affine-cipher/affine-cipher.spec.js
+++ b/exercises/practice/affine-cipher/affine-cipher.spec.js
@@ -1,4 +1,5 @@
-import { encode, decode } from './affine-cipher';
+import { describe, expect, test, xtest } from '@jest/globals';
+import { decode, encode } from './affine-cipher';
describe('Affine cipher', () => {
describe('encode', () => {
@@ -24,26 +25,29 @@ describe('Affine cipher', () => {
xtest('encode numbers', () => {
expect(encode('Testing,1 2 3, testing.', { a: 3, b: 4 })).toBe(
- 'jqgjc rw123 jqgjc rw'
+ 'jqgjc rw123 jqgjc rw',
);
});
xtest('encode deep thought', () => {
expect(encode('Truth is fiction.', { a: 5, b: 17 })).toBe(
- 'iynia fdqfb ifje'
+ 'iynia fdqfb ifje',
);
});
xtest('encode all the letters', () => {
expect(
- encode('The quick brown fox jumps over the lazy dog.', { a: 17, b: 33 })
+ encode('The quick brown fox jumps over the lazy dog.', {
+ a: 17,
+ b: 33,
+ }),
).toBe('swxtj npvyk lruol iejdc blaxk swxmh qzglf');
});
xtest('encode with a not coprime to m', () => {
expect(() => {
encode('This is a test.', { a: 6, b: 17 });
- }).toThrowError('a and m must be coprime.');
+ }).toThrow('a and m must be coprime.');
});
});
describe('decode', () => {
@@ -53,38 +57,38 @@ describe('Affine cipher', () => {
xtest('decode a sentence', () => {
expect(
- decode('qdwju nqcro muwhn odqun oppmd aunwd o', { a: 19, b: 16 })
+ decode('qdwju nqcro muwhn odqun oppmd aunwd o', { a: 19, b: 16 }),
).toBe('anobstacleisoftenasteppingstone');
});
xtest('decode numbers', () => {
expect(decode('odpoz ub123 odpoz ub', { a: 25, b: 7 })).toBe(
- 'testing123testing'
+ 'testing123testing',
);
});
xtest('decode all the letters', () => {
expect(
- decode('swxtj npvyk lruol iejdc blaxk swxmh qzglf', { a: 17, b: 33 })
+ decode('swxtj npvyk lruol iejdc blaxk swxmh qzglf', { a: 17, b: 33 }),
).toBe('thequickbrownfoxjumpsoverthelazydog');
});
xtest('decode with no spaces in input', () => {
expect(
- decode('swxtjnpvyklruoliejdcblaxkswxmhqzglf', { a: 17, b: 33 })
+ decode('swxtjnpvyklruoliejdcblaxkswxmhqzglf', { a: 17, b: 33 }),
).toBe('thequickbrownfoxjumpsoverthelazydog');
});
xtest('decode with too many spaces', () => {
expect(decode('vszzm cly yd cg qdp', { a: 15, b: 16 })).toBe(
- 'jollygreengiant'
+ 'jollygreengiant',
);
});
xtest('decode with a not coprime to m', () => {
expect(() => {
decode('Test', { a: 13, b: 5 });
- }).toThrowError('a and m must be coprime.');
+ }).toThrow('a and m must be coprime.');
});
});
});
diff --git a/exercises/practice/affine-cipher/babel.config.js b/exercises/practice/affine-cipher/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/practice/affine-cipher/babel.config.js
+++ b/exercises/practice/affine-cipher/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/practice/affine-cipher/eslint.config.mjs b/exercises/practice/affine-cipher/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/practice/affine-cipher/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/practice/affine-cipher/jest.config.js b/exercises/practice/affine-cipher/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/practice/affine-cipher/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/practice/affine-cipher/package.json b/exercises/practice/affine-cipher/package.json
index 7183040790..5114750b75 100644
--- a/exercises/practice/affine-cipher/package.json
+++ b/exercises/practice/affine-cipher/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/practice/affine-cipher"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/practice/all-your-base/.docs/instructions.md b/exercises/practice/all-your-base/.docs/instructions.md
index 858f0815e4..1b688b6915 100644
--- a/exercises/practice/all-your-base/.docs/instructions.md
+++ b/exercises/practice/all-your-base/.docs/instructions.md
@@ -1,32 +1,28 @@
# Instructions
-Convert a number, represented as a sequence of digits in one base, to any other base.
+Convert a sequence of digits in one base, representing a number, into a sequence of digits in another base, representing the same number.
-Implement general base conversion. Given a number in base **a**,
-represented as a sequence of digits, convert it to base **b**.
+~~~~exercism/note
+Try to implement the conversion yourself.
+Do not use something else to perform the conversion for you.
+~~~~
-## Note
+## About [Positional Notation][positional-notation]
-- Try to implement the conversion yourself.
- Do not use something else to perform the conversion for you.
-
-## About [Positional Notation](https://en.wikipedia.org/wiki/Positional_notation)
-
-In positional notation, a number in base **b** can be understood as a linear
-combination of powers of **b**.
+In positional notation, a number in base **b** can be understood as a linear combination of powers of **b**.
The number 42, _in base 10_, means:
-(4 _ 10^1) + (2 _ 10^0)
+`(4 × 10¹) + (2 × 10⁰)`
The number 101010, _in base 2_, means:
-(1 _ 2^5) + (0 _ 2^4) + (1 _ 2^3) + (0 _ 2^2) + (1 _ 2^1) + (0 _ 2^0)
+`(1 × 2⁵) + (0 × 2⁴) + (1 × 2³) + (0 × 2²) + (1 × 2¹) + (0 × 2⁰)`
The number 1120, _in base 3_, means:
-(1 _ 3^3) + (1 _ 3^2) + (2 _ 3^1) + (0 _ 3^0)
-
-I think you got the idea!
+`(1 × 3³) + (1 × 3²) + (2 × 3¹) + (0 × 3⁰)`
_Yes. Those three numbers above are exactly the same. Congratulations!_
+
+[positional-notation]: https://en.wikipedia.org/wiki/Positional_notation
diff --git a/exercises/practice/all-your-base/.docs/introduction.md b/exercises/practice/all-your-base/.docs/introduction.md
new file mode 100644
index 0000000000..68aaffbed9
--- /dev/null
+++ b/exercises/practice/all-your-base/.docs/introduction.md
@@ -0,0 +1,8 @@
+# Introduction
+
+You've just been hired as professor of mathematics.
+Your first week went well, but something is off in your second week.
+The problem is that every answer given by your students is wrong!
+Luckily, your math skills have allowed you to identify the problem: the student answers _are_ correct, but they're all in base 2 (binary)!
+Amazingly, it turns out that each week, the students use a different base.
+To help you quickly verify the student answers, you'll be building a tool to translate between bases.
diff --git a/exercises/practice/all-your-base/.eslintrc b/exercises/practice/all-your-base/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/practice/all-your-base/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/all-your-base/.gitignore b/exercises/practice/all-your-base/.gitignore
new file mode 100644
index 0000000000..0c88ff6ec3
--- /dev/null
+++ b/exercises/practice/all-your-base/.gitignore
@@ -0,0 +1,5 @@
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/practice/all-your-base/.meta/proof.ci.js b/exercises/practice/all-your-base/.meta/proof.ci.js
index 57eb0a2873..f7bd2176e2 100644
--- a/exercises/practice/all-your-base/.meta/proof.ci.js
+++ b/exercises/practice/all-your-base/.meta/proof.ci.js
@@ -44,7 +44,7 @@ export const convert = (array, inputBase, outputBase) => {
}
const decimalValue = array.reduce(
(accumulator, value) => accumulator * inputBase + value,
- 0
+ 0,
);
return convertFromDecimalToBase(decimalValue, outputBase);
};
diff --git a/exercises/practice/all-your-base/all-your-base.js b/exercises/practice/all-your-base/all-your-base.js
index ce43914067..644bfb7142 100644
--- a/exercises/practice/all-your-base/all-your-base.js
+++ b/exercises/practice/all-your-base/all-your-base.js
@@ -4,5 +4,5 @@
//
export const convert = () => {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
};
diff --git a/exercises/practice/all-your-base/all-your-base.spec.js b/exercises/practice/all-your-base/all-your-base.spec.js
index 8af67824b6..5c84bc8114 100644
--- a/exercises/practice/all-your-base/all-your-base.spec.js
+++ b/exercises/practice/all-your-base/all-your-base.spec.js
@@ -1,3 +1,4 @@
+import { describe, expect, test, xtest } from '@jest/globals';
import { convert } from './all-your-base';
describe('Converter', () => {
diff --git a/exercises/practice/all-your-base/babel.config.js b/exercises/practice/all-your-base/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/practice/all-your-base/babel.config.js
+++ b/exercises/practice/all-your-base/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/practice/all-your-base/eslint.config.mjs b/exercises/practice/all-your-base/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/practice/all-your-base/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/practice/all-your-base/jest.config.js b/exercises/practice/all-your-base/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/practice/all-your-base/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/practice/all-your-base/package.json b/exercises/practice/all-your-base/package.json
index c48e96fa81..77a49c3c95 100644
--- a/exercises/practice/all-your-base/package.json
+++ b/exercises/practice/all-your-base/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/practice/all-your-base"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/practice/allergies/.docs/instructions.md b/exercises/practice/allergies/.docs/instructions.md
index 4ae3818fa8..daf8cfde21 100644
--- a/exercises/practice/allergies/.docs/instructions.md
+++ b/exercises/practice/allergies/.docs/instructions.md
@@ -2,9 +2,7 @@
Given a person's allergy score, determine whether or not they're allergic to a given item, and their full list of allergies.
-An allergy test produces a single numeric score which contains the
-information about all the allergies the person has (that they were
-tested for).
+An allergy test produces a single numeric score which contains the information about all the allergies the person has (that they were tested for).
The list of items (and their value) that were tested are:
@@ -24,7 +22,6 @@ Now, given just that score of 34, your program should be able to say:
- Whether Tom is allergic to any one of those allergens listed above.
- All the allergens Tom is allergic to.
-Note: a given score may include allergens **not** listed above (i.e.
-allergens that score 256, 512, 1024, etc.). Your program should
-ignore those components of the score. For example, if the allergy
-score is 257, your program should only report the eggs (1) allergy.
+Note: a given score may include allergens **not** listed above (i.e. allergens that score 256, 512, 1024, etc.).
+Your program should ignore those components of the score.
+For example, if the allergy score is 257, your program should only report the eggs (1) allergy.
diff --git a/exercises/practice/allergies/.eslintrc b/exercises/practice/allergies/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/practice/allergies/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/allergies/.gitignore b/exercises/practice/allergies/.gitignore
new file mode 100644
index 0000000000..0c88ff6ec3
--- /dev/null
+++ b/exercises/practice/allergies/.gitignore
@@ -0,0 +1,5 @@
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/practice/allergies/.meta/config.json b/exercises/practice/allergies/.meta/config.json
index fd8d2be544..6aa4013041 100644
--- a/exercises/practice/allergies/.meta/config.json
+++ b/exercises/practice/allergies/.meta/config.json
@@ -4,6 +4,7 @@
],
"contributors": [
"ankorGH",
+ "jagdish-15",
"matthewmorgan",
"ovidiu141",
"ryanplusplus",
@@ -24,8 +25,8 @@
]
},
"blurb": "Given a person's allergy score, determine whether or not they're allergic to a given item, and their full list of allergies.",
- "source": "Jumpstart Lab Warm-up",
- "source_url": "http://jumpstartlab.com",
+ "source": "Exercise by the JumpstartLab team for students at The Turing School of Software and Design.",
+ "source_url": "https://www.turing.edu/",
"custom": {
"version.tests.compatibility": "jest-27",
"flag.tests.task-per-describe": false,
diff --git a/exercises/practice/allergies/.meta/proof.ci.js b/exercises/practice/allergies/.meta/proof.ci.js
index 9b997105a8..c3f3e1b94d 100644
--- a/exercises/practice/allergies/.meta/proof.ci.js
+++ b/exercises/practice/allergies/.meta/proof.ci.js
@@ -15,9 +15,8 @@ export class Allergies {
}
list() {
- // eslint-disable-next-line no-bitwise, no-restricted-properties
return possibleAllergies.filter(
- (allergy, i) => this.allergenIndex & Math.pow(2, i)
+ (_allergy, i) => this.allergenIndex & Math.pow(2, i),
);
}
diff --git a/exercises/practice/allergies/.meta/tests.toml b/exercises/practice/allergies/.meta/tests.toml
index 8a754c20d9..799ab8563e 100644
--- a/exercises/practice/allergies/.meta/tests.toml
+++ b/exercises/practice/allergies/.meta/tests.toml
@@ -1,150 +1,160 @@
-# This is an auto-generated file. Regular comments will be removed when this
-# file is regenerated. Regenerating will not touch any manually added keys,
-# so comments can be added in a "comment" key.
+# 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.
[17fc7296-2440-4ac4-ad7b-d07c321bc5a0]
-description = "not allergic to anything"
+description = "testing for eggs allergy -> not allergic to anything"
[07ced27b-1da5-4c2e-8ae2-cb2791437546]
-description = "allergic only to eggs"
+description = "testing for eggs allergy -> allergic only to eggs"
[5035b954-b6fa-4b9b-a487-dae69d8c5f96]
-description = "allergic to eggs and something else"
+description = "testing for eggs allergy -> allergic to eggs and something else"
[64a6a83a-5723-4b5b-a896-663307403310]
-description = "allergic to something, but not eggs"
+description = "testing for eggs allergy -> allergic to something, but not eggs"
[90c8f484-456b-41c4-82ba-2d08d93231c6]
-description = "allergic to everything"
+description = "testing for eggs allergy -> allergic to everything"
[d266a59a-fccc-413b-ac53-d57cb1f0db9d]
-description = "not allergic to anything"
+description = "testing for peanuts allergy -> not allergic to anything"
[ea210a98-860d-46b2-a5bf-50d8995b3f2a]
-description = "allergic only to peanuts"
+description = "testing for peanuts allergy -> allergic only to peanuts"
[eac69ae9-8d14-4291-ac4b-7fd2c73d3a5b]
-description = "allergic to peanuts and something else"
+description = "testing for peanuts allergy -> allergic to peanuts and something else"
[9152058c-ce39-4b16-9b1d-283ec6d25085]
-description = "allergic to something, but not peanuts"
+description = "testing for peanuts allergy -> allergic to something, but not peanuts"
[d2d71fd8-63d5-40f9-a627-fbdaf88caeab]
-description = "allergic to everything"
+description = "testing for peanuts allergy -> allergic to everything"
[b948b0a1-cbf7-4b28-a244-73ff56687c80]
-description = "not allergic to anything"
+description = "testing for shellfish allergy -> not allergic to anything"
[9ce9a6f3-53e9-4923-85e0-73019047c567]
-description = "allergic only to shellfish"
+description = "testing for shellfish allergy -> allergic only to shellfish"
[b272fca5-57ba-4b00-bd0c-43a737ab2131]
-description = "allergic to shellfish and something else"
+description = "testing for shellfish allergy -> allergic to shellfish and something else"
[21ef8e17-c227-494e-8e78-470a1c59c3d8]
-description = "allergic to something, but not shellfish"
+description = "testing for shellfish allergy -> allergic to something, but not shellfish"
[cc789c19-2b5e-4c67-b146-625dc8cfa34e]
-description = "allergic to everything"
+description = "testing for shellfish allergy -> allergic to everything"
[651bde0a-2a74-46c4-ab55-02a0906ca2f5]
-description = "not allergic to anything"
+description = "testing for strawberries allergy -> not allergic to anything"
[b649a750-9703-4f5f-b7f7-91da2c160ece]
-description = "allergic only to strawberries"
+description = "testing for strawberries allergy -> allergic only to strawberries"
[50f5f8f3-3bac-47e6-8dba-2d94470a4bc6]
-description = "allergic to strawberries and something else"
+description = "testing for strawberries allergy -> allergic to strawberries and something else"
[23dd6952-88c9-48d7-a7d5-5d0343deb18d]
-description = "allergic to something, but not strawberries"
+description = "testing for strawberries allergy -> allergic to something, but not strawberries"
[74afaae2-13b6-43a2-837a-286cd42e7d7e]
-description = "allergic to everything"
+description = "testing for strawberries allergy -> allergic to everything"
[c49a91ef-6252-415e-907e-a9d26ef61723]
-description = "not allergic to anything"
+description = "testing for tomatoes allergy -> not allergic to anything"
[b69c5131-b7d0-41ad-a32c-e1b2cc632df8]
-description = "allergic only to tomatoes"
+description = "testing for tomatoes allergy -> allergic only to tomatoes"
[1ca50eb1-f042-4ccf-9050-341521b929ec]
-description = "allergic to tomatoes and something else"
+description = "testing for tomatoes allergy -> allergic to tomatoes and something else"
[e9846baa-456b-4eff-8025-034b9f77bd8e]
-description = "allergic to something, but not tomatoes"
+description = "testing for tomatoes allergy -> allergic to something, but not tomatoes"
[b2414f01-f3ad-4965-8391-e65f54dad35f]
-description = "allergic to everything"
+description = "testing for tomatoes allergy -> allergic to everything"
[978467ab-bda4-49f7-b004-1d011ead947c]
-description = "not allergic to anything"
+description = "testing for chocolate allergy -> not allergic to anything"
[59cf4e49-06ea-4139-a2c1-d7aad28f8cbc]
-description = "allergic only to chocolate"
+description = "testing for chocolate allergy -> allergic only to chocolate"
[b0a7c07b-2db7-4f73-a180-565e07040ef1]
-description = "allergic to chocolate and something else"
+description = "testing for chocolate allergy -> allergic to chocolate and something else"
[f5506893-f1ae-482a-b516-7532ba5ca9d2]
-description = "allergic to something, but not chocolate"
+description = "testing for chocolate allergy -> allergic to something, but not chocolate"
[02debb3d-d7e2-4376-a26b-3c974b6595c6]
-description = "allergic to everything"
+description = "testing for chocolate allergy -> allergic to everything"
[17f4a42b-c91e-41b8-8a76-4797886c2d96]
-description = "not allergic to anything"
+description = "testing for pollen allergy -> not allergic to anything"
[7696eba7-1837-4488-882a-14b7b4e3e399]
-description = "allergic only to pollen"
+description = "testing for pollen allergy -> allergic only to pollen"
[9a49aec5-fa1f-405d-889e-4dfc420db2b6]
-description = "allergic to pollen and something else"
+description = "testing for pollen allergy -> allergic to pollen and something else"
[3cb8e79f-d108-4712-b620-aa146b1954a9]
-description = "allergic to something, but not pollen"
+description = "testing for pollen allergy -> allergic to something, but not pollen"
[1dc3fe57-7c68-4043-9d51-5457128744b2]
-description = "allergic to everything"
+description = "testing for pollen allergy -> allergic to everything"
[d3f523d6-3d50-419b-a222-d4dfd62ce314]
-description = "not allergic to anything"
+description = "testing for cats allergy -> not allergic to anything"
[eba541c3-c886-42d3-baef-c048cb7fcd8f]
-description = "allergic only to cats"
+description = "testing for cats allergy -> allergic only to cats"
[ba718376-26e0-40b7-bbbe-060287637ea5]
-description = "allergic to cats and something else"
+description = "testing for cats allergy -> allergic to cats and something else"
[3c6dbf4a-5277-436f-8b88-15a206f2d6c4]
-description = "allergic to something, but not cats"
+description = "testing for cats allergy -> allergic to something, but not cats"
[1faabb05-2b98-4995-9046-d83e4a48a7c1]
-description = "allergic to everything"
+description = "testing for cats allergy -> allergic to everything"
[f9c1b8e7-7dc5-4887-aa93-cebdcc29dd8f]
-description = "no allergies"
+description = "list when: -> no allergies"
[9e1a4364-09a6-4d94-990f-541a94a4c1e8]
-description = "just eggs"
+description = "list when: -> just eggs"
[8851c973-805e-4283-9e01-d0c0da0e4695]
-description = "just peanuts"
+description = "list when: -> just peanuts"
[2c8943cb-005e-435f-ae11-3e8fb558ea98]
-description = "just strawberries"
+description = "list when: -> just strawberries"
[6fa95d26-044c-48a9-8a7b-9ee46ec32c5c]
-description = "eggs and peanuts"
+description = "list when: -> eggs and peanuts"
[19890e22-f63f-4c5c-a9fb-fb6eacddfe8e]
-description = "more than eggs but not peanuts"
+description = "list when: -> more than eggs but not peanuts"
[4b68f470-067c-44e4-889f-c9fe28917d2f]
-description = "lots of stuff"
+description = "list when: -> lots of stuff"
[0881b7c5-9efa-4530-91bd-68370d054bc7]
-description = "everything"
+description = "list when: -> everything"
[12ce86de-b347-42a0-ab7c-2e0570f0c65b]
-description = "no allergen score parts"
+description = "list when: -> no allergen score parts"
+
+[93c2df3e-4f55-4fed-8116-7513092819cd]
+description = "list when: -> no allergen score parts without highest valid score"
diff --git a/exercises/practice/allergies/allergies.js b/exercises/practice/allergies/allergies.js
index acf1d5376f..f0b7ef3207 100644
--- a/exercises/practice/allergies/allergies.js
+++ b/exercises/practice/allergies/allergies.js
@@ -5,14 +5,14 @@
export class Allergies {
constructor() {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
}
list() {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
}
allergicTo() {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
}
}
diff --git a/exercises/practice/allergies/allergies.spec.js b/exercises/practice/allergies/allergies.spec.js
index 18f9f9c51c..bc11b58911 100644
--- a/exercises/practice/allergies/allergies.spec.js
+++ b/exercises/practice/allergies/allergies.spec.js
@@ -1,3 +1,4 @@
+import { describe, expect, test, xtest } from '@jest/globals';
import { Allergies } from './allergies';
describe('Allergies', () => {
@@ -285,5 +286,10 @@ describe('Allergies', () => {
'cats',
]);
});
+
+ xtest('no allergen score parts without highest valid score', () => {
+ const allergies = new Allergies(257);
+ expect(allergies.list()).toEqual(['eggs']);
+ });
});
});
diff --git a/exercises/practice/allergies/babel.config.js b/exercises/practice/allergies/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/practice/allergies/babel.config.js
+++ b/exercises/practice/allergies/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/practice/allergies/eslint.config.mjs b/exercises/practice/allergies/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/practice/allergies/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/practice/allergies/jest.config.js b/exercises/practice/allergies/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/practice/allergies/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/practice/allergies/package.json b/exercises/practice/allergies/package.json
index 007e60335a..6847f03122 100644
--- a/exercises/practice/allergies/package.json
+++ b/exercises/practice/allergies/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/practice/allergies"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/practice/alphametics/.docs/instructions.md b/exercises/practice/alphametics/.docs/instructions.md
index 6936c192d5..ef2cbb4a71 100644
--- a/exercises/practice/alphametics/.docs/instructions.md
+++ b/exercises/practice/alphametics/.docs/instructions.md
@@ -1,9 +1,8 @@
# Instructions
-Write a function to solve alphametics puzzles.
+Given an alphametics puzzle, find the correct solution.
-[Alphametics](https://en.wikipedia.org/wiki/Alphametics) is a puzzle where
-letters in words are replaced with numbers.
+[Alphametics][alphametics] is a puzzle where letters in words are replaced with numbers.
For example `SEND + MORE = MONEY`:
@@ -23,10 +22,8 @@ Replacing these with valid numbers gives:
1 0 6 5 2
```
-This is correct because every letter is replaced by a different number and the
-words, translated into numbers, then make a valid sum.
+This is correct because every letter is replaced by a different number and the words, translated into numbers, then make a valid sum.
-Each letter must represent a different digit, and the leading digit of
-a multi-digit number must not be zero.
+Each letter must represent a different digit, and the leading digit of a multi-digit number must not be zero.
-Write a function to solve alphametics puzzles.
+[alphametics]: https://en.wikipedia.org/wiki/Alphametics
diff --git a/exercises/practice/alphametics/.eslintrc b/exercises/practice/alphametics/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/practice/alphametics/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/alphametics/.gitignore b/exercises/practice/alphametics/.gitignore
new file mode 100644
index 0000000000..0c88ff6ec3
--- /dev/null
+++ b/exercises/practice/alphametics/.gitignore
@@ -0,0 +1,5 @@
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/practice/alphametics/.meta/config.json b/exercises/practice/alphametics/.meta/config.json
index 1136ab8118..25236efe0a 100644
--- a/exercises/practice/alphametics/.meta/config.json
+++ b/exercises/practice/alphametics/.meta/config.json
@@ -20,7 +20,7 @@
".meta/proof.ci.js"
]
},
- "blurb": "Write a function to solve alphametics puzzles.",
+ "blurb": "Given an alphametics puzzle, find the correct solution.",
"custom": {
"version.tests.compatibility": "jest-27",
"flag.tests.task-per-describe": false,
diff --git a/exercises/practice/alphametics/.meta/proof.ci.js b/exercises/practice/alphametics/.meta/proof.ci.js
index f982eb4543..fe9b11f9d9 100644
--- a/exercises/practice/alphametics/.meta/proof.ci.js
+++ b/exercises/practice/alphametics/.meta/proof.ci.js
@@ -38,7 +38,7 @@ function testPermutation(letterCounts, numbers) {
return counts.reduce((sum, count, i) => sum + count * numbers[i], 0) === 0
? letters.reduce(
(solution, letter, i) => ({ ...solution, [letter]: numbers[i] }),
- {}
+ {},
)
: null;
}
diff --git a/exercises/practice/alphametics/alphametics.js b/exercises/practice/alphametics/alphametics.js
index fd5babdcdc..a53b20e597 100644
--- a/exercises/practice/alphametics/alphametics.js
+++ b/exercises/practice/alphametics/alphametics.js
@@ -4,5 +4,5 @@
//
export const solve = () => {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
};
diff --git a/exercises/practice/alphametics/alphametics.spec.js b/exercises/practice/alphametics/alphametics.spec.js
index 7f7c3439de..07f990e446 100644
--- a/exercises/practice/alphametics/alphametics.spec.js
+++ b/exercises/practice/alphametics/alphametics.spec.js
@@ -1,3 +1,4 @@
+import { describe, expect, test, xtest } from '@jest/globals';
import { solve } from './alphametics';
describe('Solve the alphametics puzzle', () => {
diff --git a/exercises/practice/alphametics/babel.config.js b/exercises/practice/alphametics/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/practice/alphametics/babel.config.js
+++ b/exercises/practice/alphametics/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/practice/alphametics/eslint.config.mjs b/exercises/practice/alphametics/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/practice/alphametics/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/practice/alphametics/jest.config.js b/exercises/practice/alphametics/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/practice/alphametics/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/practice/alphametics/package.json b/exercises/practice/alphametics/package.json
index 3e26f5ba33..cf3cf1e54a 100644
--- a/exercises/practice/alphametics/package.json
+++ b/exercises/practice/alphametics/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/practice/alphametics"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/practice/anagram/.docs/instructions.append.md b/exercises/practice/anagram/.docs/instructions.append.md
new file mode 100644
index 0000000000..273ca3cbe5
--- /dev/null
+++ b/exercises/practice/anagram/.docs/instructions.append.md
@@ -0,0 +1,5 @@
+# Instructions Append
+
+## Implementation
+
+The anagrams can be returned in any order.
diff --git a/exercises/practice/anagram/.docs/instructions.md b/exercises/practice/anagram/.docs/instructions.md
index 7d1c8283ef..dca24f5262 100644
--- a/exercises/practice/anagram/.docs/instructions.md
+++ b/exercises/practice/anagram/.docs/instructions.md
@@ -1,13 +1,12 @@
# Instructions
-An anagram is a rearrangement of letters to form a new word: for example `"owns"` is an anagram of `"snow"`.
-A word is not its own anagram: for example, `"stop"` is not an anagram of `"stop"`.
+Given a target word and one or more candidate words, your task is to find the candidates that are anagrams of the target.
-Given a target word and a set of candidate words, this exercise requests the anagram set: the subset of the candidates that are anagrams of the target.
+An anagram is a rearrangement of letters to form a new word: for example `"owns"` is an anagram of `"snow"`.
+A word is _not_ its own anagram: for example, `"stop"` is not an anagram of `"stop"`.
-The target and candidates are words of one or more ASCII alphabetic characters (`A`-`Z` and `a`-`z`).
-Lowercase and uppercase characters are equivalent: for example, `"PoTS"` is an anagram of `"sTOp"`, but `StoP` is not an anagram of `sTOp`.
-The anagram set is the subset of the candidate set that are anagrams of the target (in any order).
-Words in the anagram set should have the same letter case as in the candidate set.
+The target word and candidate words are made up of one or more ASCII alphabetic characters (`A`-`Z` and `a`-`z`).
+Lowercase and uppercase characters are equivalent: for example, `"PoTS"` is an anagram of `"sTOp"`, but `"StoP"` is not an anagram of `"sTOp"`.
+The words you need to find should be taken from the candidate words, using the same letter case.
-Given the target `"stone"` and candidates `"stone"`, `"tones"`, `"banana"`, `"tons"`, `"notes"`, `"Seton"`, the anagram set is `"tones"`, `"notes"`, `"Seton"`.
+Given the target `"stone"` and the candidate words `"stone"`, `"tones"`, `"banana"`, `"tons"`, `"notes"`, and `"Seton"`, the anagram words you need to find are `"tones"`, `"notes"`, and `"Seton"`.
diff --git a/exercises/practice/anagram/.docs/introduction.md b/exercises/practice/anagram/.docs/introduction.md
new file mode 100644
index 0000000000..1acbdf00b0
--- /dev/null
+++ b/exercises/practice/anagram/.docs/introduction.md
@@ -0,0 +1,12 @@
+# Introduction
+
+At a garage sale, you find a lovely vintage typewriter at a bargain price!
+Excitedly, you rush home, insert a sheet of paper, and start typing away.
+However, your excitement wanes when you examine the output: all words are garbled!
+For example, it prints "stop" instead of "post" and "least" instead of "stale."
+Carefully, you try again, but now it prints "spot" and "slate."
+After some experimentation, you find there is a random delay before each letter is printed, which messes up the order.
+You now understand why they sold it for so little money!
+
+You realize this quirk allows you to generate anagrams, which are words formed by rearranging the letters of another word.
+Pleased with your finding, you spend the rest of the day generating hundreds of anagrams.
diff --git a/exercises/practice/anagram/.eslintrc b/exercises/practice/anagram/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/practice/anagram/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/anagram/.gitignore b/exercises/practice/anagram/.gitignore
new file mode 100644
index 0000000000..0c88ff6ec3
--- /dev/null
+++ b/exercises/practice/anagram/.gitignore
@@ -0,0 +1,5 @@
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/practice/anagram/.meta/config.json b/exercises/practice/anagram/.meta/config.json
index 86ac6e0dce..132d0a755a 100644
--- a/exercises/practice/anagram/.meta/config.json
+++ b/exercises/practice/anagram/.meta/config.json
@@ -8,6 +8,7 @@
"draalger",
"gabriel376",
"gargrave",
+ "jagdish-15",
"kytrinyx",
"matthewmorgan",
"ovidiu141",
@@ -26,7 +27,7 @@
".meta/proof.ci.js"
]
},
- "blurb": "Given a word and a list of possible anagrams, select the correct sublist.",
+ "blurb": "Find the words that use the same letters as another word.",
"source": "Inspired by the Extreme Startup game",
"source_url": "https://github.com/rchatley/extreme_startup",
"custom": {
diff --git a/exercises/practice/anagram/.meta/proof.ci.js b/exercises/practice/anagram/.meta/proof.ci.js
index 0147356fdc..ac53a2c23c 100644
--- a/exercises/practice/anagram/.meta/proof.ci.js
+++ b/exercises/practice/anagram/.meta/proof.ci.js
@@ -7,6 +7,6 @@ export const findAnagrams = (subject, candidates) => {
const wordsCopy = Array.isArray(candidates) ? candidates : [...candidates];
return wordsCopy.filter(
(candidate) =>
- !sameWord(subject, candidate) && isAnagram(subject, candidate)
+ !sameWord(subject, candidate) && isAnagram(subject, candidate),
);
};
diff --git a/exercises/practice/anagram/.meta/tests.toml b/exercises/practice/anagram/.meta/tests.toml
index 6c507d257f..4d90562705 100644
--- a/exercises/practice/anagram/.meta/tests.toml
+++ b/exercises/practice/anagram/.meta/tests.toml
@@ -46,12 +46,41 @@ description = "detects anagrams using case-insensitive possible matches"
[7cc195ad-e3c7-44ee-9fd2-d3c344806a2c]
description = "does not detect an anagram if the original word is repeated"
+include = false
+
+[630abb71-a94e-4715-8395-179ec1df9f91]
+description = "does not detect an anagram if the original word is repeated"
+reimplements = "7cc195ad-e3c7-44ee-9fd2-d3c344806a2c"
[9878a1c9-d6ea-4235-ae51-3ea2befd6842]
description = "anagrams must use all letters exactly once"
[85757361-4535-45fd-ac0e-3810d40debc1]
description = "words are not anagrams of themselves (case-insensitive)"
+include = false
+
+[68934ed0-010b-4ef9-857a-20c9012d1ebf]
+description = "words are not anagrams of themselves"
+reimplements = "85757361-4535-45fd-ac0e-3810d40debc1"
+
+[589384f3-4c8a-4e7d-9edc-51c3e5f0c90e]
+description = "words are not anagrams of themselves even if letter case is partially different"
+reimplements = "85757361-4535-45fd-ac0e-3810d40debc1"
+
+[ba53e423-7e02-41ee-9ae2-71f91e6d18e6]
+description = "words are not anagrams of themselves even if letter case is completely different"
+reimplements = "85757361-4535-45fd-ac0e-3810d40debc1"
[a0705568-628c-4b55-9798-82e4acde51ca]
description = "words other than themselves can be anagrams"
+include = false
+
+[33d3f67e-fbb9-49d3-a90e-0beb00861da7]
+description = "words other than themselves can be anagrams"
+reimplements = "a0705568-628c-4b55-9798-82e4acde51ca"
+
+[a6854f66-eec1-4afd-a137-62ef2870c051]
+description = "handles case of greek letters"
+
+[fd3509e5-e3ba-409d-ac3d-a9ac84d13296]
+description = "different characters may have the same bytes"
diff --git a/exercises/practice/anagram/anagram.js b/exercises/practice/anagram/anagram.js
index 0733d01302..2b63d207d5 100644
--- a/exercises/practice/anagram/anagram.js
+++ b/exercises/practice/anagram/anagram.js
@@ -4,5 +4,5 @@
//
export const findAnagrams = () => {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
};
diff --git a/exercises/practice/anagram/anagram.spec.js b/exercises/practice/anagram/anagram.spec.js
index bea592ea5b..dc011aea9e 100644
--- a/exercises/practice/anagram/anagram.spec.js
+++ b/exercises/practice/anagram/anagram.spec.js
@@ -1,83 +1,144 @@
+import { describe, expect, test, xtest } from '@jest/globals';
import { findAnagrams } from './anagram';
+const areSetsEqual = (setA, setB) =>
+ setA.size === setB.size && [...setA].every((val) => setB.has(val));
+
describe('Anagram', () => {
test('no matches', () => {
- expect(
- findAnagrams('diaper', ['hello', 'world', 'zombies', 'pants'])
- ).toEqual([]);
+ const expected = [];
+ const actual = findAnagrams('diaper', [
+ 'hello',
+ 'world',
+ 'zombies',
+ 'pants',
+ ]);
+ expect(areSetsEqual(new Set(expected), new Set(actual))).toEqual(true);
});
xtest('detects two anagrams', () => {
- expect(findAnagrams('solemn', ['lemons', 'cherry', 'melons'])).toEqual([
- 'lemons',
- 'melons',
- ]);
+ const expected = ['lemons', 'melons'];
+ const actual = findAnagrams('solemn', ['lemons', 'cherry', 'melons']);
+ expect(areSetsEqual(new Set(expected), new Set(actual))).toEqual(true);
});
xtest('does not detect anagram subsets', () => {
- expect(findAnagrams('good', ['dog', 'goody'])).toEqual([]);
+ const expected = [];
+ const actual = findAnagrams('good', ['dog', 'goody']);
+ expect(areSetsEqual(new Set(expected), new Set(actual))).toEqual(true);
});
xtest('detects anagram', () => {
- expect(
- findAnagrams('listen', ['enlists', 'google', 'inlets', 'banana'])
- ).toEqual(['inlets']);
+ const expected = ['inlets'];
+ const actual = findAnagrams('listen', [
+ 'enlists',
+ 'google',
+ 'inlets',
+ 'banana',
+ ]);
+ expect(areSetsEqual(new Set(expected), new Set(actual))).toEqual(true);
});
xtest('detects three anagrams', () => {
- expect(
- findAnagrams('allergy', [
- 'gallery',
- 'ballerina',
- 'regally',
- 'clergy',
- 'largely',
- 'leading',
- ])
- ).toEqual(['gallery', 'regally', 'largely']);
+ const expected = ['gallery', 'regally', 'largely'];
+ const actual = findAnagrams('allergy', [
+ 'gallery',
+ 'ballerina',
+ 'regally',
+ 'clergy',
+ 'largely',
+ 'leading',
+ ]);
+ expect(areSetsEqual(new Set(expected), new Set(actual))).toEqual(true);
});
xtest('detects multiple anagrams with different case', () => {
- expect(findAnagrams('nose', ['Eons', 'ONES'])).toEqual(['Eons', 'ONES']);
+ const expected = ['Eons', 'ONES'];
+ const actual = findAnagrams('nose', ['Eons', 'ONES']);
+ expect(areSetsEqual(new Set(expected), new Set(actual))).toEqual(true);
});
xtest('does not detect non-anagrams with identical checksum', () => {
- expect(findAnagrams('mass', ['last'])).toEqual([]);
+ const expected = [];
+ const actual = findAnagrams('mass', ['last']);
+ expect(areSetsEqual(new Set(expected), new Set(actual))).toEqual(true);
});
xtest('detects anagrams case-insensitively', () => {
- expect(
- findAnagrams('Orchestra', ['cashregister', 'Carthorse', 'radishes'])
- ).toEqual(['Carthorse']);
+ const expected = ['Carthorse'];
+ const actual = findAnagrams('Orchestra', [
+ 'cashregister',
+ 'Carthorse',
+ 'radishes',
+ ]);
+ expect(areSetsEqual(new Set(expected), new Set(actual))).toEqual(true);
});
xtest('detects anagrams using case-insensitive subject', () => {
- expect(
- findAnagrams('Orchestra', ['cashregister', 'carthorse', 'radishes'])
- ).toEqual(['carthorse']);
+ const expected = ['carthorse'];
+ const actual = findAnagrams('Orchestra', [
+ 'cashregister',
+ 'carthorse',
+ 'radishes',
+ ]);
+ expect(areSetsEqual(new Set(expected), new Set(actual))).toEqual(true);
});
xtest('detects anagrams using case-insensitive possible matches', () => {
- expect(
- findAnagrams('orchestra', ['cashregister', 'Carthorse', 'radishes'])
- ).toEqual(['Carthorse']);
+ const expected = ['Carthorse'];
+ const actual = findAnagrams('orchestra', [
+ 'cashregister',
+ 'Carthorse',
+ 'radishes',
+ ]);
+ expect(areSetsEqual(new Set(expected), new Set(actual))).toEqual(true);
});
xtest('does not detect an anagram if the original word is repeated', () => {
- expect(findAnagrams('go', ['go Go GO'])).toEqual([]);
+ const expected = [];
+ const actual = findAnagrams('go', ['goGoGO']);
+ expect(areSetsEqual(new Set(expected), new Set(actual))).toEqual(true);
});
xtest('anagrams must use all letters exactly once', () => {
- expect(findAnagrams('tapper', ['patter'])).toEqual([]);
+ const expected = [];
+ const actual = findAnagrams('tapper', ['patter']);
+ expect(areSetsEqual(new Set(expected), new Set(actual))).toEqual(true);
});
- xtest('words are not anagrams of themselves (case-insensitive)', () => {
- expect(findAnagrams('BANANA', ['BANANA', 'Banana', 'banana'])).toEqual([]);
+ xtest('words are not anagrams of themselves', () => {
+ const expected = [];
+ const actual = findAnagrams('BANANA', ['BANANA']);
+ expect(areSetsEqual(new Set(expected), new Set(actual))).toEqual(true);
+ });
+
+ xtest('words are not anagrams of themselves even if letter case is partially different', () => {
+ const expected = [];
+ const actual = findAnagrams('BANANA', ['Banana']);
+ expect(areSetsEqual(new Set(expected), new Set(actual))).toEqual(true);
+ });
+
+ xtest('words are not anagrams of themselves even if letter case is completely different', () => {
+ const expected = [];
+ const actual = findAnagrams('BANANA', ['banana']);
+ expect(areSetsEqual(new Set(expected), new Set(actual))).toEqual(true);
});
xtest('words other than themselves can be anagrams', () => {
- expect(findAnagrams('LISTEN', ['Listen', 'Silent', 'LISTEN'])).toEqual([
- 'Silent',
- ]);
+ const expected = ['Silent'];
+ const actual = findAnagrams('LISTEN', ['LISTEN', 'Silent']);
+ expect(areSetsEqual(new Set(expected), new Set(actual))).toEqual(true);
+ });
+
+ xtest('handles case of greek letters', () => {
+ const expected = ['ΒΓΑ', 'γβα'];
+ const actual = findAnagrams('ΑΒΓ', ['ΒΓΑ', 'ΒΓΔ', 'γβα', 'αβγ']);
+ expect(areSetsEqual(new Set(expected), new Set(actual))).toEqual(true);
+ });
+
+ xtest('different characters may have the same bytes', () => {
+ const expected = [];
+ const actual = findAnagrams('a⬂', ['€a']);
+ expect(areSetsEqual(new Set(expected), new Set(actual))).toEqual(true);
});
});
diff --git a/exercises/practice/anagram/babel.config.js b/exercises/practice/anagram/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/practice/anagram/babel.config.js
+++ b/exercises/practice/anagram/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/practice/anagram/eslint.config.mjs b/exercises/practice/anagram/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/practice/anagram/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/practice/anagram/jest.config.js b/exercises/practice/anagram/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/practice/anagram/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/practice/anagram/package.json b/exercises/practice/anagram/package.json
index cc168d2653..ae36a8c88f 100644
--- a/exercises/practice/anagram/package.json
+++ b/exercises/practice/anagram/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/practice/anagram"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/practice/armstrong-numbers/.docs/instructions.append.md b/exercises/practice/armstrong-numbers/.docs/instructions.append.md
new file mode 100644
index 0000000000..e4b24f9e9b
--- /dev/null
+++ b/exercises/practice/armstrong-numbers/.docs/instructions.append.md
@@ -0,0 +1,10 @@
+# Instructions append
+
+## Implementation
+
+
+~~~~exercism/note
+Some of the tests might pass a `BigInt` as input.
+Ensure that your implementation can handle such cases.
+~~~~
+
diff --git a/exercises/practice/armstrong-numbers/.docs/instructions.md b/exercises/practice/armstrong-numbers/.docs/instructions.md
index b5c46e6fe2..5e56bbe465 100644
--- a/exercises/practice/armstrong-numbers/.docs/instructions.md
+++ b/exercises/practice/armstrong-numbers/.docs/instructions.md
@@ -1,6 +1,6 @@
# Instructions
-An [Armstrong number](https://en.wikipedia.org/wiki/Narcissistic_number) is a number that is the sum of its own digits each raised to the power of the number of digits.
+An [Armstrong number][armstrong-number] is a number that is the sum of its own digits each raised to the power of the number of digits.
For example:
@@ -10,3 +10,5 @@ For example:
- 154 is _not_ an Armstrong number, because: `154 != 1^3 + 5^3 + 4^3 = 1 + 125 + 64 = 190`
Write some code to determine whether a number is an Armstrong number.
+
+[armstrong-number]: https://en.wikipedia.org/wiki/Narcissistic_number
diff --git a/exercises/practice/armstrong-numbers/.eslintrc b/exercises/practice/armstrong-numbers/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/practice/armstrong-numbers/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/armstrong-numbers/.gitignore b/exercises/practice/armstrong-numbers/.gitignore
new file mode 100644
index 0000000000..0c88ff6ec3
--- /dev/null
+++ b/exercises/practice/armstrong-numbers/.gitignore
@@ -0,0 +1,5 @@
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/practice/armstrong-numbers/.meta/config.json b/exercises/practice/armstrong-numbers/.meta/config.json
index 157629c906..2b559e8013 100644
--- a/exercises/practice/armstrong-numbers/.meta/config.json
+++ b/exercises/practice/armstrong-numbers/.meta/config.json
@@ -6,6 +6,7 @@
"ankorGH",
"gargrave",
"hayashi-ay",
+ "jagdish-15",
"ovidiu141",
"SleeplessByte",
"xarxziux"
@@ -21,7 +22,7 @@
".meta/proof.ci.js"
]
},
- "blurb": "Determine if a number is an Armstrong number",
+ "blurb": "Determine if a number is an Armstrong number.",
"source": "Wikipedia",
"source_url": "https://en.wikipedia.org/wiki/Narcissistic_number",
"custom": {
diff --git a/exercises/practice/armstrong-numbers/.meta/proof.ci.js b/exercises/practice/armstrong-numbers/.meta/proof.ci.js
index 0fe35c82e6..720db16d0d 100644
--- a/exercises/practice/armstrong-numbers/.meta/proof.ci.js
+++ b/exercises/practice/armstrong-numbers/.meta/proof.ci.js
@@ -1,8 +1,12 @@
export const isArmstrongNumber = (input) => {
- const digits = [...String(input)];
+ const bigInput = BigInt(input);
+
+ const digits = [...String(bigInput)];
+
const sum = digits.reduce(
- (total, current) => total + current ** digits.length,
- 0
+ (total, current) => total + BigInt(current) ** BigInt(digits.length),
+ BigInt(0),
);
- return sum === input;
+
+ return sum === bigInput;
};
diff --git a/exercises/practice/armstrong-numbers/.meta/tests.toml b/exercises/practice/armstrong-numbers/.meta/tests.toml
index fdada6d1ef..b3f09e4cc9 100644
--- a/exercises/practice/armstrong-numbers/.meta/tests.toml
+++ b/exercises/practice/armstrong-numbers/.meta/tests.toml
@@ -1,30 +1,43 @@
-# This is an auto-generated file. Regular comments will be removed when this
-# file is regenerated. Regenerating will not touch any manually added keys,
-# so comments can be added in a "comment" key.
+# 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.
[c1ed103c-258d-45b2-be73-d8c6d9580c7b]
description = "Zero is an Armstrong number"
[579e8f03-9659-4b85-a1a2-d64350f6b17a]
-description = "Single digit numbers are Armstrong numbers"
+description = "Single-digit numbers are Armstrong numbers"
[2d6db9dc-5bf8-4976-a90b-b2c2b9feba60]
-description = "There are no 2 digit Armstrong numbers"
+description = "There are no two-digit Armstrong numbers"
[509c087f-e327-4113-a7d2-26a4e9d18283]
-description = "Three digit number that is an Armstrong number"
+description = "Three-digit number that is an Armstrong number"
[7154547d-c2ce-468d-b214-4cb953b870cf]
-description = "Three digit number that is not an Armstrong number"
+description = "Three-digit number that is not an Armstrong number"
[6bac5b7b-42e9-4ecb-a8b0-4832229aa103]
-description = "Four digit number that is an Armstrong number"
+description = "Four-digit number that is an Armstrong number"
[eed4b331-af80-45b5-a80b-19c9ea444b2e]
-description = "Four digit number that is not an Armstrong number"
+description = "Four-digit number that is not an Armstrong number"
[f971ced7-8d68-4758-aea1-d4194900b864]
-description = "Seven digit number that is an Armstrong number"
+description = "Seven-digit number that is an Armstrong number"
[7ee45d52-5d35-4fbd-b6f1-5c8cd8a67f18]
-description = "Seven digit number that is not an Armstrong number"
+description = "Seven-digit number that is not an Armstrong number"
+
+[5ee2fdf8-334e-4a46-bb8d-e5c19c02c148]
+description = "Armstrong number containing seven zeroes"
+
+[12ffbf10-307a-434e-b4ad-c925680e1dd4]
+description = "The largest and last Armstrong number"
diff --git a/exercises/practice/armstrong-numbers/armstrong-numbers.js b/exercises/practice/armstrong-numbers/armstrong-numbers.js
index d3ff1cdc3b..31eba9f6f4 100644
--- a/exercises/practice/armstrong-numbers/armstrong-numbers.js
+++ b/exercises/practice/armstrong-numbers/armstrong-numbers.js
@@ -4,5 +4,5 @@
//
export const isArmstrongNumber = () => {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
};
diff --git a/exercises/practice/armstrong-numbers/armstrong-numbers.spec.js b/exercises/practice/armstrong-numbers/armstrong-numbers.spec.js
index 192664789e..4d380e8b7d 100644
--- a/exercises/practice/armstrong-numbers/armstrong-numbers.spec.js
+++ b/exercises/practice/armstrong-numbers/armstrong-numbers.spec.js
@@ -1,3 +1,4 @@
+import { describe, expect, test, xtest } from '@jest/globals';
import { isArmstrongNumber } from './armstrong-numbers';
describe('Armstrong Numbers', () => {
@@ -5,35 +6,45 @@ describe('Armstrong Numbers', () => {
expect(isArmstrongNumber(0)).toEqual(true);
});
- xtest('Single digit numbers are Armstrong numbers', () => {
+ xtest('Single-digit numbers are Armstrong numbers', () => {
expect(isArmstrongNumber(5)).toEqual(true);
});
- xtest('There are no 2 digit Armstrong numbers', () => {
+ xtest('There are no two-digit Armstrong numbers', () => {
expect(isArmstrongNumber(10)).toEqual(false);
});
- xtest('Three digit number that is an Armstrong number', () => {
+ xtest('Three-digit number that is an Armstrong number', () => {
expect(isArmstrongNumber(153)).toEqual(true);
});
- xtest('Three digit number that is not an Armstrong number', () => {
+ xtest('Three-digit number that is not an Armstrong number', () => {
expect(isArmstrongNumber(100)).toEqual(false);
});
- xtest('Four digit number that is an Armstrong number', () => {
+ xtest('Four-digit number that is an Armstrong number', () => {
expect(isArmstrongNumber(9474)).toEqual(true);
});
- xtest('Four digit number that is not an Armstrong number', () => {
+ xtest('Four-digit number that is not an Armstrong number', () => {
expect(isArmstrongNumber(9475)).toEqual(false);
});
- xtest('Seven digit number that is an Armstrong number', () => {
+ xtest('Seven-digit number that is an Armstrong number', () => {
expect(isArmstrongNumber(9926315)).toEqual(true);
});
- xtest('Seven digit number that is not an Armstrong number', () => {
+ xtest('Seven-digit number that is not an Armstrong number', () => {
expect(isArmstrongNumber(9926314)).toEqual(false);
});
+
+ xtest('Armstrong number containing seven zeroes', () => {
+ const bigInput = 186709961001538790100634132976990n;
+ expect(isArmstrongNumber(bigInput)).toEqual(true);
+ });
+
+ xtest('The largest and last Armstrong number', () => {
+ const bigInput = 115132219018763992565095597973971522401n;
+ expect(isArmstrongNumber(bigInput)).toEqual(true);
+ });
});
diff --git a/exercises/practice/armstrong-numbers/babel.config.js b/exercises/practice/armstrong-numbers/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/practice/armstrong-numbers/babel.config.js
+++ b/exercises/practice/armstrong-numbers/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/practice/armstrong-numbers/eslint.config.mjs b/exercises/practice/armstrong-numbers/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/practice/armstrong-numbers/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/practice/armstrong-numbers/jest.config.js b/exercises/practice/armstrong-numbers/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/practice/armstrong-numbers/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/practice/armstrong-numbers/package.json b/exercises/practice/armstrong-numbers/package.json
index 19e320ec06..a4fa2a3024 100644
--- a/exercises/practice/armstrong-numbers/package.json
+++ b/exercises/practice/armstrong-numbers/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/practice/armstrong-numbers"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/practice/atbash-cipher/.docs/instructions.md b/exercises/practice/atbash-cipher/.docs/instructions.md
index 2f712b1592..1e7627b1e5 100644
--- a/exercises/practice/atbash-cipher/.docs/instructions.md
+++ b/exercises/practice/atbash-cipher/.docs/instructions.md
@@ -1,11 +1,9 @@
# Instructions
-Create an implementation of the atbash cipher, an ancient encryption system created in the Middle East.
+Create an implementation of the Atbash cipher, an ancient encryption system created in the Middle East.
-The Atbash cipher is a simple substitution cipher that relies on
-transposing all the letters in the alphabet such that the resulting
-alphabet is backwards. The first letter is replaced with the last
-letter, the second with the second-last, and so on.
+The Atbash cipher is a simple substitution cipher that relies on transposing all the letters in the alphabet such that the resulting alphabet is backwards.
+The first letter is replaced with the last letter, the second with the second-last, and so on.
An Atbash cipher for the Latin alphabet would be as follows:
@@ -14,16 +12,16 @@ Plain: abcdefghijklmnopqrstuvwxyz
Cipher: zyxwvutsrqponmlkjihgfedcba
```
-It is a very weak cipher because it only has one possible key, and it is
-a simple monoalphabetic substitution cipher. However, this may not have
-been an issue in the cipher's time.
+It is a very weak cipher because it only has one possible key, and it is a simple mono-alphabetic substitution cipher.
+However, this may not have been an issue in the cipher's time.
-Ciphertext is written out in groups of fixed length, the traditional group size
-being 5 letters, and punctuation is excluded. This is to make it harder to guess
-things based on word boundaries.
+Ciphertext is written out in groups of fixed length, the traditional group size being 5 letters, leaving numbers unchanged, and punctuation is excluded.
+This is to make it harder to guess things based on word boundaries.
+All text will be encoded as lowercase letters.
## Examples
- Encoding `test` gives `gvhg`
+- Encoding `x123 yes` gives `c123b vh`
- Decoding `gvhg` gives `test`
- Decoding `gsvjf rxpyi ldmul cqfnk hlevi gsvoz abwlt` gives `thequickbrownfoxjumpsoverthelazydog`
diff --git a/exercises/practice/atbash-cipher/.eslintrc b/exercises/practice/atbash-cipher/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/practice/atbash-cipher/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/atbash-cipher/.gitignore b/exercises/practice/atbash-cipher/.gitignore
new file mode 100644
index 0000000000..0c88ff6ec3
--- /dev/null
+++ b/exercises/practice/atbash-cipher/.gitignore
@@ -0,0 +1,5 @@
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/practice/atbash-cipher/.meta/config.json b/exercises/practice/atbash-cipher/.meta/config.json
index f7faba2764..13d409345e 100644
--- a/exercises/practice/atbash-cipher/.meta/config.json
+++ b/exercises/practice/atbash-cipher/.meta/config.json
@@ -22,9 +22,9 @@
".meta/proof.ci.js"
]
},
- "blurb": "Create an implementation of the atbash cipher, an ancient encryption system created in the Middle East.",
+ "blurb": "Create an implementation of the Atbash cipher, an ancient encryption system created in the Middle East.",
"source": "Wikipedia",
- "source_url": "http://en.wikipedia.org/wiki/Atbash",
+ "source_url": "https://en.wikipedia.org/wiki/Atbash",
"custom": {
"version.tests.compatibility": "jest-27",
"flag.tests.task-per-describe": false,
diff --git a/exercises/practice/atbash-cipher/atbash-cipher.js b/exercises/practice/atbash-cipher/atbash-cipher.js
index 18545fb900..ef98d1956a 100644
--- a/exercises/practice/atbash-cipher/atbash-cipher.js
+++ b/exercises/practice/atbash-cipher/atbash-cipher.js
@@ -4,9 +4,9 @@
//
export const encode = () => {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
};
export const decode = () => {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
};
diff --git a/exercises/practice/atbash-cipher/atbash-cipher.spec.js b/exercises/practice/atbash-cipher/atbash-cipher.spec.js
index 250fa84a1f..fd68ae2ad2 100644
--- a/exercises/practice/atbash-cipher/atbash-cipher.spec.js
+++ b/exercises/practice/atbash-cipher/atbash-cipher.spec.js
@@ -1,4 +1,5 @@
-import { encode, decode } from './atbash-cipher';
+import { describe, expect, test, xtest } from '@jest/globals';
+import { decode, encode } from './atbash-cipher';
describe('Atbash Cipher', () => {
describe('encode', () => {
diff --git a/exercises/practice/atbash-cipher/babel.config.js b/exercises/practice/atbash-cipher/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/practice/atbash-cipher/babel.config.js
+++ b/exercises/practice/atbash-cipher/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/practice/atbash-cipher/eslint.config.mjs b/exercises/practice/atbash-cipher/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/practice/atbash-cipher/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/practice/atbash-cipher/jest.config.js b/exercises/practice/atbash-cipher/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/practice/atbash-cipher/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/practice/atbash-cipher/package.json b/exercises/practice/atbash-cipher/package.json
index aac5a13a04..185dcd9021 100644
--- a/exercises/practice/atbash-cipher/package.json
+++ b/exercises/practice/atbash-cipher/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/practice/atbash-cipher"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/practice/bank-account/.docs/instructions.md b/exercises/practice/bank-account/.docs/instructions.md
index 1265ac8b34..7398fbea18 100644
--- a/exercises/practice/bank-account/.docs/instructions.md
+++ b/exercises/practice/bank-account/.docs/instructions.md
@@ -1,27 +1,10 @@
# Instructions
-Simulate a bank account supporting opening/closing, withdrawals, and deposits
-of money. Watch out for concurrent transactions!
+Your task is to implement bank accounts supporting opening/closing, withdrawals, and deposits of money.
-A bank account can be accessed in multiple ways. Clients can make
-deposits and withdrawals using the internet, mobile phones, etc. Shops
-can charge against the account.
+As bank accounts can be accessed in many different ways (internet, mobile phones, automatic charges), your bank software must allow accounts to be safely accessed from multiple threads/processes (terminology depends on your programming language) in parallel.
+For example, there may be many deposits and withdrawals occurring in parallel; you need to ensure there are no [race conditions][wikipedia] between when you read the account balance and set the new balance.
-Create an account that can be accessed from multiple threads/processes
-(terminology depends on your programming language).
+It should be possible to close an account; operations against a closed account must fail.
-It should be possible to close an account; operations against a closed
-account must fail.
-
-## Instructions
-
-Run the test file, and fix each of the errors in turn. When you get the
-first test to pass, go to the first pending or skipped test, and make
-that pass as well. When all of the tests are passing, feel free to
-submit.
-
-Remember that passing code is just the first step. The goal is to work
-towards a solution that is as readable and expressive as you can make
-it.
-
-Have fun!
+[wikipedia]: https://en.wikipedia.org/wiki/Race_condition#In_software
diff --git a/exercises/practice/bank-account/.docs/introduction.md b/exercises/practice/bank-account/.docs/introduction.md
new file mode 100644
index 0000000000..650b5d9c46
--- /dev/null
+++ b/exercises/practice/bank-account/.docs/introduction.md
@@ -0,0 +1,20 @@
+# Introduction
+
+After years of filling out forms and waiting, you've finally acquired your banking license.
+This means you are now officially eligible to open your own bank, hurray!
+
+Your first priority is to get the IT systems up and running.
+After a day of hard work, you can already open and close accounts, as well as handle withdrawals and deposits.
+
+Since you couldn't be bothered writing tests, you invite some friends to help test the system.
+However, after just five minutes, one of your friends claims they've lost money!
+While you're confident your code is bug-free, you start looking through the logs to investigate.
+
+Ah yes, just as you suspected, your friend is at fault!
+They shared their test credentials with another friend, and together they conspired to make deposits and withdrawals from the same account _in parallel_.
+Who would do such a thing?
+
+While you argue that it's physically _impossible_ for someone to access their account in parallel, your friend smugly notifies you that the banking rules _require_ you to support this.
+Thus, no parallel banking support, no go-live signal.
+Sighing, you create a mental note to work on this tomorrow.
+This will set your launch date back at _least_ one more day, but well...
diff --git a/exercises/practice/bank-account/.eslintrc b/exercises/practice/bank-account/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/practice/bank-account/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/bank-account/.gitignore b/exercises/practice/bank-account/.gitignore
new file mode 100644
index 0000000000..0c88ff6ec3
--- /dev/null
+++ b/exercises/practice/bank-account/.gitignore
@@ -0,0 +1,5 @@
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/practice/bank-account/.meta/config.json b/exercises/practice/bank-account/.meta/config.json
index 23ae37c0b5..330828347b 100644
--- a/exercises/practice/bank-account/.meta/config.json
+++ b/exercises/practice/bank-account/.meta/config.json
@@ -3,6 +3,7 @@
"TomPradat"
],
"contributors": [
+ "jagdish-15",
"SleeplessByte"
],
"files": {
diff --git a/exercises/practice/bank-account/.meta/tests.toml b/exercises/practice/bank-account/.meta/tests.toml
new file mode 100644
index 0000000000..4e42d4dcb5
--- /dev/null
+++ b/exercises/practice/bank-account/.meta/tests.toml
@@ -0,0 +1,61 @@
+# 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.
+
+[983a1528-4ceb-45e5-8257-8ce01aceb5ed]
+description = "Newly opened account has zero balance"
+
+[e88d4ec3-c6bf-4752-8e59-5046c44e3ba7]
+description = "Single deposit"
+
+[3d9147d4-63f4-4844-8d2b-1fee2e9a2a0d]
+description = "Multiple deposits"
+
+[08f1af07-27ae-4b38-aa19-770bde558064]
+description = "Withdraw once"
+
+[6f6d242f-8c31-4ac6-8995-a90d42cad59f]
+description = "Withdraw twice"
+
+[45161c94-a094-4c77-9cec-998b70429bda]
+description = "Can do multiple operations sequentially"
+
+[f9facfaa-d824-486e-8381-48832c4bbffd]
+description = "Cannot check balance of closed account"
+
+[7a65ba52-e35c-4fd2-8159-bda2bde6e59c]
+description = "Cannot deposit into closed account"
+
+[a0a1835d-faae-4ad4-a6f3-1fcc2121380b]
+description = "Cannot deposit into unopened account"
+
+[570dfaa5-0532-4c1f-a7d3-0f65c3265608]
+description = "Cannot withdraw from closed account"
+
+[c396d233-1c49-4272-98dc-7f502dbb9470]
+description = "Cannot close an account that was not opened"
+
+[c06f534f-bdc2-4a02-a388-1063400684de]
+description = "Cannot open an already opened account"
+
+[0722d404-6116-4f92-ba3b-da7f88f1669c]
+description = "Reopened account does not retain balance"
+
+[ec42245f-9361-4341-8231-a22e8d19c52f]
+description = "Cannot withdraw more than deposited"
+
+[4f381ef8-10ef-4507-8e1d-0631ecc8ee72]
+description = "Cannot withdraw negative"
+
+[d45df9ea-1db0-47f3-b18c-d365db49d938]
+description = "Cannot deposit negative"
+
+[ba0c1e0b-0f00-416f-8097-a7dfc97871ff]
+description = "Can handle concurrent transactions"
diff --git a/exercises/practice/bank-account/babel.config.js b/exercises/practice/bank-account/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/practice/bank-account/babel.config.js
+++ b/exercises/practice/bank-account/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/practice/bank-account/bank-account.js b/exercises/practice/bank-account/bank-account.js
index 8c2448180a..7ce11bf802 100644
--- a/exercises/practice/bank-account/bank-account.js
+++ b/exercises/practice/bank-account/bank-account.js
@@ -5,27 +5,27 @@
export class BankAccount {
constructor() {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
}
open() {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
}
close() {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
}
deposit() {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
}
withdraw() {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
}
get balance() {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
}
}
diff --git a/exercises/practice/bank-account/bank-account.spec.js b/exercises/practice/bank-account/bank-account.spec.js
index 59ef05f8fb..406027a249 100644
--- a/exercises/practice/bank-account/bank-account.spec.js
+++ b/exercises/practice/bank-account/bank-account.spec.js
@@ -1,3 +1,4 @@
+import { describe, expect, test, xtest } from '@jest/globals';
import { BankAccount, ValueError } from './bank-account';
describe('Bank Account', () => {
@@ -7,14 +8,14 @@ describe('Bank Account', () => {
expect(account.balance).toEqual(0);
});
- xtest('can deposit money', () => {
+ xtest('Single deposit', () => {
const account = new BankAccount();
account.open();
account.deposit(100);
expect(account.balance).toEqual(100);
});
- xtest('can deposit money sequentially', () => {
+ xtest('Multiple deposits"', () => {
const account = new BankAccount();
account.open();
account.deposit(100);
@@ -22,7 +23,7 @@ describe('Bank Account', () => {
expect(account.balance).toEqual(150);
});
- xtest('can withdraw money', () => {
+ xtest('Withdraw once', () => {
const account = new BankAccount();
account.open();
account.deposit(100);
@@ -30,7 +31,7 @@ describe('Bank Account', () => {
expect(account.balance).toEqual(50);
});
- xtest('can withdraw money sequentially', () => {
+ xtest('Withdraw twice', () => {
const account = new BankAccount();
account.open();
account.deposit(100);
@@ -39,14 +40,25 @@ describe('Bank Account', () => {
expect(account.balance).toEqual(0);
});
- xtest('checking balance of closed account throws error', () => {
+ xtest('Can do multiple operations sequentially', () => {
+ const account = new BankAccount();
+ account.open();
+ account.deposit(100);
+ account.deposit(110);
+ account.withdraw(200);
+ account.deposit(60);
+ account.withdraw(50);
+ expect(account.balance).toEqual(20);
+ });
+
+ xtest('Cannot check balance of closed account', () => {
const account = new BankAccount();
account.open();
account.close();
expect(() => account.balance).toThrow(ValueError);
});
- xtest('deposit into closed account throws error', () => {
+ xtest('Cannot deposit into closed account', () => {
const account = new BankAccount();
account.open();
account.close();
@@ -55,7 +67,14 @@ describe('Bank Account', () => {
}).toThrow(ValueError);
});
- xtest('withdraw from closed account throws error', () => {
+ xtest('Cannot deposit into unopened account', () => {
+ const account = new BankAccount();
+ expect(() => {
+ account.deposit(50);
+ }).toThrow(ValueError);
+ });
+
+ xtest('Cannot withdraw from closed account', () => {
const account = new BankAccount();
account.open();
account.close();
@@ -64,14 +83,14 @@ describe('Bank Account', () => {
}).toThrow(ValueError);
});
- xtest('close already closed account throws error', () => {
+ xtest('Cannot close an account that was not opened', () => {
const account = new BankAccount();
expect(() => {
account.close();
}).toThrow(ValueError);
});
- xtest('open already opened account throws error', () => {
+ xtest('Cannot open an already opened account', () => {
const account = new BankAccount();
account.open();
expect(() => {
@@ -79,7 +98,7 @@ describe('Bank Account', () => {
}).toThrow(ValueError);
});
- xtest('reopened account does not retain balance', () => {
+ xtest('Reopened account does not retain balance', () => {
const account = new BankAccount();
account.open();
account.deposit(50);
@@ -88,7 +107,7 @@ describe('Bank Account', () => {
expect(account.balance).toEqual(0);
});
- xtest('cannot withdraw more than deposited', () => {
+ xtest('Cannot withdraw more than deposited', () => {
const account = new BankAccount();
account.open();
account.deposit(25);
@@ -97,7 +116,7 @@ describe('Bank Account', () => {
}).toThrow(ValueError);
});
- xtest('cannot withdraw negative amount', () => {
+ xtest('Cannot withdraw negative', () => {
const account = new BankAccount();
account.open();
account.deposit(100);
@@ -106,7 +125,7 @@ describe('Bank Account', () => {
}).toThrow(ValueError);
});
- xtest('cannot deposit negative amount', () => {
+ xtest('Cannot deposit negative', () => {
const account = new BankAccount();
account.open();
expect(() => {
@@ -114,7 +133,40 @@ describe('Bank Account', () => {
}).toThrow(ValueError);
});
- xtest('changing balance directly throws error', () => {
+ xtest('Can handle concurrent transactions', async () => {
+ const account = new BankAccount();
+ account.open();
+ account.deposit(1000);
+
+ for (let i = 0; i < 10; i++) {
+ await adjustBalanceConcurrently(account);
+ expect(account.balance).toEqual(1000);
+ }
+ });
+
+ function adjustBalanceConcurrently(account) {
+ const random = () => Math.floor(Math.random() * 10);
+
+ const tasks = Array.from(
+ { length: 1000 },
+ () =>
+ new Promise((resolve) => {
+ try {
+ account.deposit(5);
+ setTimeout(() => {
+ account.withdraw(5);
+ resolve();
+ }, random());
+ } catch (e) {
+ throw new Error(`Exception should not be thrown: ${e.message}`);
+ }
+ }),
+ );
+
+ return Promise.all(tasks);
+ }
+
+ xtest('Changing balance directly throws error', () => {
const account = new BankAccount();
account.open();
expect(() => {
diff --git a/exercises/practice/bank-account/eslint.config.mjs b/exercises/practice/bank-account/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/practice/bank-account/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/practice/bank-account/jest.config.js b/exercises/practice/bank-account/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/practice/bank-account/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/practice/bank-account/package.json b/exercises/practice/bank-account/package.json
index 51a290f2a1..36967fdcfe 100644
--- a/exercises/practice/bank-account/package.json
+++ b/exercises/practice/bank-account/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/practice/bank-account"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/practice/beer-song/.docs/instructions.md b/exercises/practice/beer-song/.docs/instructions.md
index 5680128293..e909cfe317 100644
--- a/exercises/practice/beer-song/.docs/instructions.md
+++ b/exercises/practice/beer-song/.docs/instructions.md
@@ -305,17 +305,3 @@ Take it down and pass it around, no more bottles of beer on the wall.
No more bottles of beer on the wall, no more bottles of beer.
Go to the store and buy some more, 99 bottles of beer on the wall.
```
-
-## For bonus points
-
-Did you get the tests passing and the code clean? If you want to, these
-are some additional things you could try:
-
-- Remove as much duplication as you possibly can.
-- Optimize for readability, even if it means introducing duplication.
-- If you've removed all the duplication, do you have a lot of
- conditionals? Try replacing the conditionals with polymorphism, if it
- applies in this language. How readable is it?
-
-Then please share your thoughts in a comment on the submission. Did this
-experiment make the code better? Worse? Did you learn anything from it?
diff --git a/exercises/practice/beer-song/.eslintrc b/exercises/practice/beer-song/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/practice/beer-song/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/beer-song/.gitignore b/exercises/practice/beer-song/.gitignore
new file mode 100644
index 0000000000..0c88ff6ec3
--- /dev/null
+++ b/exercises/practice/beer-song/.gitignore
@@ -0,0 +1,5 @@
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/practice/beer-song/.meta/config.json b/exercises/practice/beer-song/.meta/config.json
index 8e777dde7e..dd543caec9 100644
--- a/exercises/practice/beer-song/.meta/config.json
+++ b/exercises/practice/beer-song/.meta/config.json
@@ -27,7 +27,7 @@
},
"blurb": "Produce the lyrics to that beloved classic, that field-trip favorite: 99 Bottles of Beer on the Wall.",
"source": "Learn to Program by Chris Pine",
- "source_url": "http://pine.fm/LearnToProgram/?Chapter=06",
+ "source_url": "https://pine.fm/LearnToProgram/?Chapter=06",
"custom": {
"version.tests.compatibility": "jest-27",
"flag.tests.task-per-describe": false,
diff --git a/exercises/practice/beer-song/.meta/proof.ci.js b/exercises/practice/beer-song/.meta/proof.ci.js
index 1f38385c64..92db51db27 100644
--- a/exercises/practice/beer-song/.meta/proof.ci.js
+++ b/exercises/practice/beer-song/.meta/proof.ci.js
@@ -25,13 +25,13 @@ function nextVerse(currentVerse) {
function nextBottle(currentVerse) {
return `${bottles(
- nextVerse(currentVerse)
+ nextVerse(currentVerse),
).toLowerCase()} of beer on the wall.`;
}
function verse(number) {
const line1 = `${bottles(number)} of beer on the wall, ${bottles(
- number
+ number,
).toLowerCase()} of beer.`;
const line2 = action(number) + nextBottle(number);
diff --git a/exercises/practice/beer-song/babel.config.js b/exercises/practice/beer-song/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/practice/beer-song/babel.config.js
+++ b/exercises/practice/beer-song/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/practice/beer-song/beer-song.js b/exercises/practice/beer-song/beer-song.js
index 8340bc23bd..0c93f00a4e 100644
--- a/exercises/practice/beer-song/beer-song.js
+++ b/exercises/practice/beer-song/beer-song.js
@@ -4,5 +4,5 @@
//
export const recite = (initialBottlesCount, takeDownCount) => {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
};
diff --git a/exercises/practice/beer-song/beer-song.spec.js b/exercises/practice/beer-song/beer-song.spec.js
index c02ee0d55d..9738c5fe25 100644
--- a/exercises/practice/beer-song/beer-song.spec.js
+++ b/exercises/practice/beer-song/beer-song.spec.js
@@ -1,3 +1,4 @@
+import { describe, expect, test, xtest } from '@jest/globals';
import { recite } from './beer-song';
describe('Beer Song', () => {
diff --git a/exercises/practice/beer-song/eslint.config.mjs b/exercises/practice/beer-song/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/practice/beer-song/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/practice/beer-song/jest.config.js b/exercises/practice/beer-song/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/practice/beer-song/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/practice/beer-song/package.json b/exercises/practice/beer-song/package.json
index bb50cc92ad..b60e4c6e54 100644
--- a/exercises/practice/beer-song/package.json
+++ b/exercises/practice/beer-song/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/practice/beer-song"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/practice/binary-search-tree/.docs/instructions.md b/exercises/practice/binary-search-tree/.docs/instructions.md
index ba3c42eb67..7625220e9a 100644
--- a/exercises/practice/binary-search-tree/.docs/instructions.md
+++ b/exercises/practice/binary-search-tree/.docs/instructions.md
@@ -2,53 +2,69 @@
Insert and search for numbers in a binary tree.
-When we need to represent sorted data, an array does not make a good
-data structure.
-
-Say we have the array `[1, 3, 4, 5]`, and we add 2 to it so it becomes
-`[1, 3, 4, 5, 2]` now we must sort the entire array again! We can
-improve on this by realizing that we only need to make space for the new
-item `[1, nil, 3, 4, 5]`, and then adding the item in the space we
-added. But this still requires us to shift many elements down by one.
-
-Binary Search Trees, however, can operate on sorted data much more
-efficiently.
-
-A binary search tree consists of a series of connected nodes. Each node
-contains a piece of data (e.g. the number 3), a variable named `left`,
-and a variable named `right`. The `left` and `right` variables point at
-`nil`, or other nodes. Since these other nodes in turn have other nodes
-beneath them, we say that the left and right variables are pointing at
-subtrees. All data in the left subtree is less than or equal to the
-current node's data, and all data in the right subtree is greater than
-the current node's data.
-
-For example, if we had a node containing the data 4, and we added the
-data 2, our tree would look like this:
+When we need to represent sorted data, an array does not make a good data structure.
+Say we have the array `[1, 3, 4, 5]`, and we add 2 to it so it becomes `[1, 3, 4, 5, 2]`.
+Now we must sort the entire array again!
+We can improve on this by realizing that we only need to make space for the new item `[1, nil, 3, 4, 5]`, and then adding the item in the space we added.
+But this still requires us to shift many elements down by one.
+
+Binary Search Trees, however, can operate on sorted data much more efficiently.
+
+A binary search tree consists of a series of connected nodes.
+Each node contains a piece of data (e.g. the number 3), a variable named `left`, and a variable named `right`.
+The `left` and `right` variables point at `nil`, or other nodes.
+Since these other nodes in turn have other nodes beneath them, we say that the left and right variables are pointing at subtrees.
+All data in the left subtree is less than or equal to the current node's data, and all data in the right subtree is greater than the current node's data.
+
+For example, if we had a node containing the data 4, and we added the data 2, our tree would look like this:
+
+
+
+```text
4
/
2
+```
If we then added 6, it would look like this:
+
+
+```text
4
/ \
2 6
+```
If we then added 3, it would look like this
+
+
+```text
4
/ \
2 6
\
3
+```
And if we then added 1, 5, and 7, it would look like this
+
+
+```text
4
/ \
/ \
2 6
/ \ / \
1 3 5 7
+```
+
+## Credit
+
+The images were created by [habere-et-dispertire][habere-et-dispertire] using [PGF/TikZ][pgf-tikz] by Till Tantau.
+
+[habere-et-dispertire]: https://exercism.org/profiles/habere-et-dispertire
+[pgf-tikz]: https://en.wikipedia.org/wiki/PGF/TikZ
diff --git a/exercises/practice/binary-search-tree/.eslintrc b/exercises/practice/binary-search-tree/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/practice/binary-search-tree/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/binary-search-tree/.gitignore b/exercises/practice/binary-search-tree/.gitignore
new file mode 100644
index 0000000000..0c88ff6ec3
--- /dev/null
+++ b/exercises/practice/binary-search-tree/.gitignore
@@ -0,0 +1,5 @@
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/practice/binary-search-tree/.meta/config.json b/exercises/practice/binary-search-tree/.meta/config.json
index 60507ab35c..21afb3ab79 100644
--- a/exercises/practice/binary-search-tree/.meta/config.json
+++ b/exercises/practice/binary-search-tree/.meta/config.json
@@ -23,7 +23,6 @@
},
"blurb": "Insert and search for numbers in a binary tree.",
"source": "Josh Cheek",
- "source_url": "https://twitter.com/josh_cheek",
"custom": {
"version.tests.compatibility": "jest-27",
"flag.tests.task-per-describe": false,
diff --git a/exercises/practice/binary-search-tree/babel.config.js b/exercises/practice/binary-search-tree/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/practice/binary-search-tree/babel.config.js
+++ b/exercises/practice/binary-search-tree/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/practice/binary-search-tree/binary-search-tree.js b/exercises/practice/binary-search-tree/binary-search-tree.js
index eeb74c3a3e..bcfa41475c 100644
--- a/exercises/practice/binary-search-tree/binary-search-tree.js
+++ b/exercises/practice/binary-search-tree/binary-search-tree.js
@@ -5,25 +5,25 @@
export class BinarySearchTree {
constructor() {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
}
get data() {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
}
get right() {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
}
get left() {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
}
insert() {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
}
each() {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
}
}
diff --git a/exercises/practice/binary-search-tree/binary-search-tree.spec.js b/exercises/practice/binary-search-tree/binary-search-tree.spec.js
index 658451d1a0..1f207a0a1b 100644
--- a/exercises/practice/binary-search-tree/binary-search-tree.spec.js
+++ b/exercises/practice/binary-search-tree/binary-search-tree.spec.js
@@ -1,3 +1,4 @@
+import { describe, expect, test, xtest } from '@jest/globals';
import { BinarySearchTree } from './binary-search-tree';
function recordAllData(bst) {
diff --git a/exercises/practice/binary-search-tree/eslint.config.mjs b/exercises/practice/binary-search-tree/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/practice/binary-search-tree/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/practice/binary-search-tree/jest.config.js b/exercises/practice/binary-search-tree/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/practice/binary-search-tree/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/practice/binary-search-tree/package.json b/exercises/practice/binary-search-tree/package.json
index e5af3245c5..cf756b91c4 100644
--- a/exercises/practice/binary-search-tree/package.json
+++ b/exercises/practice/binary-search-tree/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/practice/binary-search-tree"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/practice/binary-search/.docs/instructions.md b/exercises/practice/binary-search/.docs/instructions.md
index 4dcaba726a..12f4358ebc 100644
--- a/exercises/practice/binary-search/.docs/instructions.md
+++ b/exercises/practice/binary-search/.docs/instructions.md
@@ -1,35 +1,29 @@
# Instructions
-Implement a binary search algorithm.
+Your task is to implement a binary search algorithm.
-Searching a sorted collection is a common task. A dictionary is a sorted
-list of word definitions. Given a word, one can find its definition. A
-telephone book is a sorted list of people's names, addresses, and
-telephone numbers. Knowing someone's name allows one to quickly find
-their telephone number and address.
+A binary search algorithm finds an item in a list by repeatedly splitting it in half, only keeping the half which contains the item we're looking for.
+It allows us to quickly narrow down the possible locations of our item until we find it, or until we've eliminated all possible locations.
-If the list to be searched contains more than a few items (a dozen, say)
-a binary search will require far fewer comparisons than a linear search,
-but it imposes the requirement that the list be sorted.
+~~~~exercism/caution
+Binary search only works when a list has been sorted.
+~~~~
-In computer science, a binary search or half-interval search algorithm
-finds the position of a specified input value (the search "key") within
-an array sorted by key value.
+The algorithm looks like this:
-In each step, the algorithm compares the search key value with the key
-value of the middle element of the array.
+- Find the middle element of a _sorted_ list and compare it with the item we're looking for.
+- If the middle element is our item, then we're done!
+- If the middle element is greater than our item, we can eliminate that element and all the elements **after** it.
+- If the middle element is less than our item, we can eliminate that element and all the elements **before** it.
+- If every element of the list has been eliminated then the item is not in the list.
+- Otherwise, repeat the process on the part of the list that has not been eliminated.
-If the keys match, then a matching element has been found and its index,
-or position, is returned.
+Here's an example:
-Otherwise, if the search key is less than the middle element's key, then
-the algorithm repeats its action on the sub-array to the left of the
-middle element or, if the search key is greater, on the sub-array to the
-right.
+Let's say we're looking for the number 23 in the following sorted list: `[4, 8, 12, 16, 23, 28, 32]`.
-If the remaining array to be searched is empty, then the key cannot be
-found in the array and a special "not found" indication is returned.
-
-A binary search halves the number of items to check with each iteration,
-so locating an item (or determining its absence) takes logarithmic time.
-A binary search is a dichotomic divide and conquer search algorithm.
+- We start by comparing 23 with the middle element, 16.
+- Since 23 is greater than 16, we can eliminate the left half of the list, leaving us with `[23, 28, 32]`.
+- We then compare 23 with the new middle element, 28.
+- Since 23 is less than 28, we can eliminate the right half of the list: `[23]`.
+- We've found our item.
diff --git a/exercises/practice/binary-search/.docs/introduction.md b/exercises/practice/binary-search/.docs/introduction.md
new file mode 100644
index 0000000000..03496599e7
--- /dev/null
+++ b/exercises/practice/binary-search/.docs/introduction.md
@@ -0,0 +1,13 @@
+# Introduction
+
+You have stumbled upon a group of mathematicians who are also singer-songwriters.
+They have written a song for each of their favorite numbers, and, as you can imagine, they have a lot of favorite numbers (like [0][zero] or [73][seventy-three] or [6174][kaprekars-constant]).
+
+You are curious to hear the song for your favorite number, but with so many songs to wade through, finding the right song could take a while.
+Fortunately, they have organized their songs in a playlist sorted by the title — which is simply the number that the song is about.
+
+You realize that you can use a binary search algorithm to quickly find a song given the title.
+
+[zero]: https://en.wikipedia.org/wiki/0
+[seventy-three]: https://en.wikipedia.org/wiki/73_(number)
+[kaprekars-constant]: https://en.wikipedia.org/wiki/6174_(number)
diff --git a/exercises/practice/binary-search/.eslintrc b/exercises/practice/binary-search/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/practice/binary-search/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/binary-search/.gitignore b/exercises/practice/binary-search/.gitignore
new file mode 100644
index 0000000000..0c88ff6ec3
--- /dev/null
+++ b/exercises/practice/binary-search/.gitignore
@@ -0,0 +1,5 @@
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/practice/binary-search/.meta/config.json b/exercises/practice/binary-search/.meta/config.json
index d5e96cb749..fcd3cd81e4 100644
--- a/exercises/practice/binary-search/.meta/config.json
+++ b/exercises/practice/binary-search/.meta/config.json
@@ -23,7 +23,7 @@
},
"blurb": "Implement a binary search algorithm.",
"source": "Wikipedia",
- "source_url": "http://en.wikipedia.org/wiki/Binary_search_algorithm",
+ "source_url": "https://en.wikipedia.org/wiki/Binary_search_algorithm",
"custom": {
"version.tests.compatibility": "jest-27",
"flag.tests.task-per-describe": false,
diff --git a/exercises/practice/binary-search/babel.config.js b/exercises/practice/binary-search/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/practice/binary-search/babel.config.js
+++ b/exercises/practice/binary-search/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/practice/binary-search/binary-search.js b/exercises/practice/binary-search/binary-search.js
index 88e2fa3063..4589a19e66 100644
--- a/exercises/practice/binary-search/binary-search.js
+++ b/exercises/practice/binary-search/binary-search.js
@@ -4,5 +4,5 @@
//
export const find = () => {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
};
diff --git a/exercises/practice/binary-search/binary-search.spec.js b/exercises/practice/binary-search/binary-search.spec.js
index 0444899681..a216ba93a2 100644
--- a/exercises/practice/binary-search/binary-search.spec.js
+++ b/exercises/practice/binary-search/binary-search.spec.js
@@ -1,3 +1,4 @@
+import { describe, expect, test, xtest } from '@jest/globals';
import { find } from './binary-search';
describe('Binary Search', () => {
diff --git a/exercises/practice/binary-search/eslint.config.mjs b/exercises/practice/binary-search/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/practice/binary-search/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/practice/binary-search/jest.config.js b/exercises/practice/binary-search/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/practice/binary-search/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/practice/binary-search/package.json b/exercises/practice/binary-search/package.json
index 78a1e6477d..eadee75896 100644
--- a/exercises/practice/binary-search/package.json
+++ b/exercises/practice/binary-search/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/practice/binary-search"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/practice/binary/.eslintrc b/exercises/practice/binary/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/practice/binary/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/binary/.gitignore b/exercises/practice/binary/.gitignore
new file mode 100644
index 0000000000..0c88ff6ec3
--- /dev/null
+++ b/exercises/practice/binary/.gitignore
@@ -0,0 +1,5 @@
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/practice/binary/babel.config.js b/exercises/practice/binary/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/practice/binary/babel.config.js
+++ b/exercises/practice/binary/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/practice/binary/binary.js b/exercises/practice/binary/binary.js
index dfbca26ec8..d5fc4e68e5 100644
--- a/exercises/practice/binary/binary.js
+++ b/exercises/practice/binary/binary.js
@@ -5,10 +5,10 @@
export class Binary {
constructor() {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
}
toDecimal() {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
}
}
diff --git a/exercises/practice/binary/binary.spec.js b/exercises/practice/binary/binary.spec.js
index 4cc07f4fc1..415daed688 100644
--- a/exercises/practice/binary/binary.spec.js
+++ b/exercises/practice/binary/binary.spec.js
@@ -1,3 +1,4 @@
+import { describe, test, expect, xtest } from '@jest/globals';
import { Binary } from './binary';
describe('binary', () => {
diff --git a/exercises/practice/binary/eslint.config.mjs b/exercises/practice/binary/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/practice/binary/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/practice/binary/jest.config.js b/exercises/practice/binary/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/practice/binary/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/practice/binary/package.json b/exercises/practice/binary/package.json
index 701644f7d6..904cd2ab7f 100644
--- a/exercises/practice/binary/package.json
+++ b/exercises/practice/binary/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/practice/binary"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/practice/bob/.approaches/answer-array/content.md b/exercises/practice/bob/.approaches/answer-array/content.md
new file mode 100644
index 0000000000..8063c663d9
--- /dev/null
+++ b/exercises/practice/bob/.approaches/answer-array/content.md
@@ -0,0 +1,73 @@
+# Answer array
+
+```javascript
+const answers = [
+ 'Whatever.',
+ 'Sure.',
+ 'Whoa, chill out!',
+ "Calm down, I know what I'm doing!",
+];
+
+export function hey(message) {
+ const speech = message.trimEnd();
+ if (speech == '') {
+ return 'Fine. Be that way!';
+ }
+
+ const isQuestion = speech.endsWith('?') ? 1 : 0;
+ const isShout =
+ /[A-Z]{1}/.test(speech) && speech == speech.toUpperCase() ? 2 : 0;
+ return answers[isQuestion + isShout];
+}
+```
+
+In this approach you define an array that contains Bob’s answers, and each condition is given a score.
+The correct answer is selected from the array by using the score as the array index.
+
+The [`String`][string] [trimEnd][trimend] method is applied to the input to eliminate any whitespace at the end of the input.
+If the string has no characters left, it returns the response for saying nothing.
+
+
+~~~~exercism/caution
+Note that a `null` or `undefined` `String` would be different from a `String` of all whitespace.
+A `null` or `undefined` `String` would raise a `TypeError` if `trimEnd` were applied to it.
+~~~~
+
+
+The first half of the shout condition
+
+```
+/[A-Z]{1}/.test(speech)
+```
+
+is constructed from a [regular expression pattern][regex] to ensure there is at least one uppercase letter character in the `String`.
+This is because the second half of the condition tests that the uppercased input is the same as the input.
+If the input were only `"123"` it would equal itself uppercased, but without letters it would not be a shout.
+
+The uppercasing is done by using the `String` method [toUpperCase][touppercase].
+
+The conditions of being a question and being a shout are assigned scores through the use of the [ternary operator][ternary].
+For example, giving a question a score of `1` would use an index of `1` to get the element from the answers array, which is `"Sure."`.
+
+| isShout | isQuestion | Index | Answer |
+| ------- | ---------- | --------- | ------------------------------------- |
+| `false` | `false` | 0 + 0 = 0 | `"Whatever."` |
+| `false` | `true` | 0 + 1 = 1 | `"Sure."` |
+| `true` | `false` | 2 + 0 = 2 | `"Whoa, chill out!"` |
+| `true` | `true` | 2 + 1 = 3 | `"Calm down, I know what I'm doing!"` |
+
+## Shortening
+
+Note that when the body of an `if` statement is a short single line, both the test expression and the body could be put on the same line, like so
+
+```javascript
+if (speech == '') return 'Fine. Be that way!';
+```
+
+It may not comply with some coding styles, but your team preferences may allow it.
+
+[string]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String
+[trimend]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trimEnd
+[regex]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
+[touppercase]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase
+[ternary]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator
diff --git a/exercises/practice/bob/.approaches/answer-array/snippet.txt b/exercises/practice/bob/.approaches/answer-array/snippet.txt
new file mode 100644
index 0000000000..757cacabef
--- /dev/null
+++ b/exercises/practice/bob/.approaches/answer-array/snippet.txt
@@ -0,0 +1,8 @@
+const answers = ["Whatever.", "Sure.", "Whoa, chill out!", "Calm down, I know what I'm doing!"];
+
+const speech = message.trim()
+if (speech == "") return "Fine. Be that way!"
+
+const isQuestion = speech.endsWith("?") ? 1: 0
+const isShout = /[A-Z]{1}/.test(speech) && speech == speech.toUpperCase() ? 2 : 0
+return answers[isQuestion + isShout]
diff --git a/exercises/practice/bob/.approaches/config.json b/exercises/practice/bob/.approaches/config.json
new file mode 100644
index 0000000000..6bb7d60e90
--- /dev/null
+++ b/exercises/practice/bob/.approaches/config.json
@@ -0,0 +1,36 @@
+{
+ "introduction": {
+ "authors": [
+ "bobahop"
+ ]
+ },
+ "approaches": [
+ {
+ "uuid": "a4775bb1-cad6-4523-b9ec-d0dfda2e98ad",
+ "slug": "if-statements",
+ "title": "If statements",
+ "blurb": "Use if statements to return the answer.",
+ "authors": [
+ "bobahop"
+ ]
+ },
+ {
+ "uuid": "a439f836-57c3-4b99-995f-d76a0321e4b8",
+ "slug": "switch-statement",
+ "title": "Switch statement",
+ "blurb": "Use a switch to return the answer.",
+ "authors": [
+ "bobahop"
+ ]
+ },
+ {
+ "uuid": "ad78cacc-d3e1-4b3d-a2e1-5488495f590c",
+ "slug": "answer-array",
+ "title": "Answer array",
+ "blurb": "Index into an array to return the answer.",
+ "authors": [
+ "bobahop"
+ ]
+ }
+ ]
+}
diff --git a/exercises/practice/bob/.approaches/if-statements/content.md b/exercises/practice/bob/.approaches/if-statements/content.md
new file mode 100644
index 0000000000..1f73996e4e
--- /dev/null
+++ b/exercises/practice/bob/.approaches/if-statements/content.md
@@ -0,0 +1,77 @@
+# `if` statements
+
+```javascript
+export function hey(message) {
+ const speech = message.trimEnd();
+ if (speech == "") {
+ return "Fine. Be that way!";
+ }
+
+ const isQuestion = speech.endsWith("?");
+ const isShout = /[A-Z]{1}/.test(speech) && speech == speech.toUpperCase();
+
+ if (isShout) {
+ return isQuestion
+ ? "Calm down, I know what I'm doing!"
+ : "Whoa, chill out!";
+ }
+ if (isQuestion) {
+ return "Sure.";
+ }
+ return "Whatever.";
+```
+
+In this approach you have a series of `if` statements using the private methods to evaluate the conditions.
+As soon as the right condition is found, the correct response is returned.
+
+
+~~~~exercism/note
+Note that there are no `else if` or `else` statements.
+If an `if` statement can return, then an `else if` or `else` is not needed.
+Execution will either return or will continue to the next statement anyway.
+~~~~
+
+
+The [`String`][string] [trimEnd][trimend] method is applied to the input to eliminate any whitespace at the end of the input.
+If the string has no characters left, it returns the response for saying nothing.
+
+The `String` method [endsWith][endswith] is used to determine if the input ends with a question mark.
+
+
+~~~~exercism/caution
+Note that a `null` or `undefined` `String` would be different from a `String` of all whitespace.
+A `null` or `undefined` `String` would raise a `TypeError` if `trimEnd` were applied to it.
+~~~~
+
+
+The first half of the shout condition
+
+```javascript
+/[A-Z]{1}/.test(speech);
+```
+
+is constructed from a [regular expression pattern][regex] to ensure there is at least one uppercase letter character in the `String`.
+This is because the second half of the condition tests that the uppercased input is the same as the input.
+If the input were only `"123"` it would equal itself uppercased, but without letters it would not be a shout.
+
+The uppercasing is done by using the `String` method [toUpperCase][touppercase].
+
+If `isShout ` is `true`, a [ternary operator][ternary] is used to return the response for whether `isQuestion` is also `true`,
+or only `isShout` is true.
+
+## Shortening
+
+Note that when the body of an `if` statement is a short single line, both the test expression and the body could be put on the same line, like so
+
+```javascript
+if (speech == '') return 'Fine. Be that way!';
+```
+
+It may not comply with some coding styles, but your team preferences may allow it.
+
+[string]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String
+[trimend]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trimEnd
+[endswith]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith
+[regex]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
+[touppercase]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase
+[ternary]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator
diff --git a/exercises/practice/bob/.approaches/if-statements/snippet.txt b/exercises/practice/bob/.approaches/if-statements/snippet.txt
new file mode 100644
index 0000000000..aee2a14553
--- /dev/null
+++ b/exercises/practice/bob/.approaches/if-statements/snippet.txt
@@ -0,0 +1,7 @@
+if (speech == "")
+ return "Fine. Be that way!"
+if (isShout)
+ return isQuestion? "Calm down, I know what I'm doing!" : "Whoa, chill out!"
+if (isQuestion)
+ return "Sure."
+return "Whatever."
diff --git a/exercises/practice/bob/.approaches/introduction.md b/exercises/practice/bob/.approaches/introduction.md
new file mode 100644
index 0000000000..c566425f6b
--- /dev/null
+++ b/exercises/practice/bob/.approaches/introduction.md
@@ -0,0 +1,101 @@
+# Introduction
+
+There are various idiomatic approaches to solve Bob.
+A basic approach can use a series of `if` statements to test the conditions.
+Or a `switch` statement can be used.
+An array can contain answers from which the right response is selected by an index calculated from scores given to the conditions.
+
+## General guidance
+
+Regardless of the approach used, some things you could look out for include
+
+- If the input is trimmed, [trim][trim] only once.
+
+- Use the [endsWith][endswith] `String` method instead of checking the last character by index for `?`.
+
+- Don't copy/paste the logic for determining a shout and for determing a question into determing a shouted question.
+ Combine the two determinations instead of copying them.
+ Not duplicating the code will keep the code [DRY][dry].
+
+- Perhaps consider making `IsQuestion` and `IsShout` values set once instead of functions that are possibly called twice.
+
+- If an `if` statement can return, then an `else if` or `else` is not needed.
+ Execution will either return or will continue to the next statement anyway.
+
+- If the body of an `if` statement is only one line, curly braces aren't needed.
+ Some teams may still require them in their style guidelines, though.
+
+## Approach: `if` statements
+
+```javascript
+export function hey(message) {
+ const speech = message.trimEnd();
+ if (speech == "") {
+ return "Fine. Be that way!";
+ }
+
+ const isQuestion = speech.endsWith("?");
+ const isShout = /[A-Z]{1}/.test(speech) && speech == speech.toUpperCase();
+
+ if (isShout) {
+ return isQuestion
+ ? "Calm down, I know what I'm doing!"
+ : "Whoa, chill out!";
+ }
+ if (isQuestion) {
+ return "Sure.";
+ }
+ return "Whatever.";
+```
+
+For more information, check the [`if` statements approach][approach-if].
+
+## Approach: `switch` statement
+
+```javascript
+export function hey(message) {
+ const speech = message.trim();
+ if (speech == '') {
+ return 'Fine. Be that way!';
+ }
+
+ const isQuestion = speech.endsWith('?');
+ const isShout = /[A-Z]{1}/.test(speech) && speech == speech.toUpperCase();
+
+ switch (true) {
+ case isQuestion && isShout:
+ return "Calm down, I know what I'm doing!";
+ case isShout:
+ return 'Whoa, chill out!';
+ case isQuestion:
+ return 'Sure.';
+ default:
+ return 'Whatever.';
+ }
+}
+```
+
+For more information, check the [`switch` statement approach][approach-switch].
+
+## Other approaches
+
+Besides the aforementioned, idiomatic approaches, you could also approach the exercise as follows:
+
+### Other approach: answer array
+
+An array can be defined that contains Bob’s answers, and each condition is given a score.
+The correct answer is selected from the array by using the score as the array index.
+For more information, check the [Answer array approach][approach-answer-array].
+
+## Which approach to use?
+
+Which to use is pretty much a matter of personal preference.
+Some may prefer the answer array approach to avoid the series of `if` statements or `switch` cases,
+but some may find the answer array approach to be less readable.
+
+[trim]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trim
+[endswith]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith
+[dry]: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself
+[approach-if]: https://exercism.org/tracks/javascript/exercises/bob/approaches/if-statements
+[approach-switch]: https://exercism.org/tracks/javascript/exercises/bob/approaches/switch-statement
+[approach-answer-array]: https://exercism.org/tracks/javascript/exercises/bob/approaches/answer-array
diff --git a/exercises/practice/bob/.approaches/switch-statement/content.md b/exercises/practice/bob/.approaches/switch-statement/content.md
new file mode 100644
index 0000000000..c683b96123
--- /dev/null
+++ b/exercises/practice/bob/.approaches/switch-statement/content.md
@@ -0,0 +1,69 @@
+# `switch` statement
+
+```javascript
+export function hey(message) {
+ const speech = message.trim();
+ if (speech == '') {
+ return 'Fine. Be that way!';
+ }
+
+ const isQuestion = speech.endsWith('?');
+ const isShout = /[A-Z]{1}/.test(speech) && speech == speech.toUpperCase();
+
+ switch (true) {
+ case isQuestion && isShout:
+ return "Calm down, I know what I'm doing!";
+ case isShout:
+ return 'Whoa, chill out!';
+ case isQuestion:
+ return 'Sure.';
+ default:
+ return 'Whatever.';
+ }
+}
+```
+
+In this approach you use a [`switch`][switch] statement to test if there is a question or a shout.
+The `switch` returns the right response for a question, shout, shouted question, or for not being a shout or question.
+
+The [`String`][string] [trimEnd][trimend] method is applied to the input to eliminate any whitespace at the end of the input.
+If the string has no characters left, it returns the response for saying nothing.
+
+
+~~~~exercism/caution
+Note that a `null` or `undefined` `String` would be different from a `String` of all whitespace.
+A `null` or `undefined` `String` would raise a `TypeError` if `trimEnd` were applied to it.
+~~~~
+
+
+The first half of the shout condition
+
+```javascript
+/[A-Z]{1}/.test(speech);
+```
+
+is constructed from a [regular expression pattern][regex] to ensure there is at least one uppercase letter character in the `String`.
+This is because the second half of the condition tests that the uppercased input is the same as the input.
+If the input were only `"123"` it would equal itself uppercased, but without letters it would not be a shout.
+
+The uppercasing is done by using the `String` method [toUpperCase][touppercase].
+
+The `isQuestion` and `isShout` values are tested in a `switch`.
+If neither `isQuestion` and `isShout` are `true`, the `default` arm of the `switch` returns the response when the input is neither a question nor a shout.
+
+## Shortening
+
+Note that when the body of an `if` statement is a short single line, both the test expression and the body could be put on the same line, like so
+
+```javascript
+if (input == '') return 'Fine. Be that way!';
+```
+
+It may not comply with some coding styles, but your team preferences may allow it.
+
+[switch]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/switch
+[string]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String
+[trimend]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trimEnd
+[regex]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
+[touppercase]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase
+[ternary]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator
diff --git a/exercises/practice/bob/.approaches/switch-statement/snippet.txt b/exercises/practice/bob/.approaches/switch-statement/snippet.txt
new file mode 100644
index 0000000000..737789bc09
--- /dev/null
+++ b/exercises/practice/bob/.approaches/switch-statement/snippet.txt
@@ -0,0 +1,7 @@
+switch (true) {
+ case isQuestion && isShout:
+ return "Calm down, I know what I'm doing!"
+ case isShout:
+ return "Whoa, chill out!"
+ case isQuestion:
+ return "Sure."
diff --git a/exercises/practice/bob/.docs/instructions.md b/exercises/practice/bob/.docs/instructions.md
index edddb1413d..bb702f7bbe 100644
--- a/exercises/practice/bob/.docs/instructions.md
+++ b/exercises/practice/bob/.docs/instructions.md
@@ -1,16 +1,19 @@
# Instructions
-Bob is a lackadaisical teenager. In conversation, his responses are very limited.
+Your task is to determine what Bob will reply to someone when they say something to him or ask him a question.
-Bob answers 'Sure.' if you ask him a question, such as "How are you?".
+Bob only ever answers one of five things:
-He answers 'Whoa, chill out!' if you YELL AT HIM (in all capitals).
-
-He answers 'Calm down, I know what I'm doing!' if you yell a question at him.
-
-He says 'Fine. Be that way!' if you address him without actually saying
-anything.
-
-He answers 'Whatever.' to anything else.
-
-Bob's conversational partner is a purist when it comes to written communication and always follows normal rules regarding sentence punctuation in English.
+- **"Sure."**
+ This is his response if you ask him a question, such as "How are you?"
+ The convention used for questions is that it ends with a question mark.
+- **"Whoa, chill out!"**
+ This is his answer if you YELL AT HIM.
+ The convention used for yelling is ALL CAPITAL LETTERS.
+- **"Calm down, I know what I'm doing!"**
+ This is what he says if you yell a question at him.
+- **"Fine. Be that way!"**
+ This is how he responds to silence.
+ The convention used for silence is nothing, or various combinations of whitespace characters.
+- **"Whatever."**
+ This is what he answers to anything else.
diff --git a/exercises/practice/bob/.docs/introduction.md b/exercises/practice/bob/.docs/introduction.md
new file mode 100644
index 0000000000..ea4a80776b
--- /dev/null
+++ b/exercises/practice/bob/.docs/introduction.md
@@ -0,0 +1,10 @@
+# Introduction
+
+Bob is a [lackadaisical][] teenager.
+He likes to think that he's very cool.
+And he definitely doesn't get excited about things.
+That wouldn't be cool.
+
+When people talk to him, his responses are pretty limited.
+
+[lackadaisical]: https://www.collinsdictionary.com/dictionary/english/lackadaisical
diff --git a/exercises/practice/bob/.eslintrc b/exercises/practice/bob/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/practice/bob/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/bob/.gitignore b/exercises/practice/bob/.gitignore
new file mode 100644
index 0000000000..0c88ff6ec3
--- /dev/null
+++ b/exercises/practice/bob/.gitignore
@@ -0,0 +1,5 @@
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/practice/bob/.meta/config.json b/exercises/practice/bob/.meta/config.json
index 37b5617870..2c8cd78fcb 100644
--- a/exercises/practice/bob/.meta/config.json
+++ b/exercises/practice/bob/.meta/config.json
@@ -8,6 +8,7 @@
"brendan-c",
"draalger",
"hayashi-ay",
+ "jagdish-15",
"kytrinyx",
"matthewmorgan",
"ryanplusplus",
@@ -28,7 +29,7 @@
},
"blurb": "Bob is a lackadaisical teenager. In conversation, his responses are very limited.",
"source": "Inspired by the 'Deaf Grandma' exercise in Chris Pine's Learn to Program tutorial.",
- "source_url": "http://pine.fm/LearnToProgram/?Chapter=06",
+ "source_url": "https://pine.fm/LearnToProgram/chap_06.html",
"custom": {
"version.tests.compatibility": "jest-27",
"flag.tests.task-per-describe": false,
diff --git a/exercises/practice/bob/.meta/tests.toml b/exercises/practice/bob/.meta/tests.toml
index 6304855792..5299e2895f 100644
--- a/exercises/practice/bob/.meta/tests.toml
+++ b/exercises/practice/bob/.meta/tests.toml
@@ -1,6 +1,13 @@
-# This is an auto-generated file. Regular comments will be removed when this
-# file is regenerated. Regenerating will not touch any manually added keys,
-# so comments can be added in a "comment" key.
+# 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.
[e162fead-606f-437a-a166-d051915cea8e]
description = "stating something"
@@ -64,6 +71,7 @@ description = "alternate silence"
[66953780-165b-4e7e-8ce3-4bcb80b6385a]
description = "multiple line question"
+include = false
[5371ef75-d9ea-4103-bcfa-2da973ddec1b]
description = "starting with whitespace"
@@ -76,3 +84,7 @@ description = "other whitespace"
[12983553-8601-46a8-92fa-fcaa3bc4a2a0]
description = "non-question ending with whitespace"
+
+[2c7278ac-f955-4eb4-bf8f-e33eb4116a15]
+description = "multiple line question"
+reimplements = "66953780-165b-4e7e-8ce3-4bcb80b6385a"
diff --git a/exercises/practice/bob/babel.config.js b/exercises/practice/bob/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/practice/bob/babel.config.js
+++ b/exercises/practice/bob/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/practice/bob/bob.js b/exercises/practice/bob/bob.js
index cc8a6fe97a..fde9cb9c80 100644
--- a/exercises/practice/bob/bob.js
+++ b/exercises/practice/bob/bob.js
@@ -4,5 +4,5 @@
//
export const hey = (message) => {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
};
diff --git a/exercises/practice/bob/bob.spec.js b/exercises/practice/bob/bob.spec.js
index eeba00d1a1..f423c26044 100644
--- a/exercises/practice/bob/bob.spec.js
+++ b/exercises/practice/bob/bob.spec.js
@@ -1,3 +1,4 @@
+import { describe, expect, test, xtest } from '@jest/globals';
import { hey } from './bob';
describe('Bob', () => {
@@ -102,8 +103,8 @@ describe('Bob', () => {
});
xtest('multiple line question', () => {
- const result = hey('\nDoes this cryogenic chamber make me look fat?\nno');
- expect(result).toEqual('Whatever.');
+ const result = hey('\nDoes this cryogenic chamber make\n me look fat?');
+ expect(result).toEqual('Sure.');
});
xtest('starting with whitespace', () => {
diff --git a/exercises/practice/bob/eslint.config.mjs b/exercises/practice/bob/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/practice/bob/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/practice/bob/jest.config.js b/exercises/practice/bob/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/practice/bob/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/practice/bob/package.json b/exercises/practice/bob/package.json
index 06bd3b8efb..b6f54db62e 100644
--- a/exercises/practice/bob/package.json
+++ b/exercises/practice/bob/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/practice/bob"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/practice/book-store/.docs/instructions.append.md b/exercises/practice/book-store/.docs/instructions.append.md
index 0f51b54f7e..6ed73ca080 100644
--- a/exercises/practice/book-store/.docs/instructions.append.md
+++ b/exercises/practice/book-store/.docs/instructions.append.md
@@ -1,7 +1,9 @@
-# Implementation
+# Instructions append
-Define a function - `Cost` - that calculates the cost for a given list of books based on defined discounts.
+## Implementation
-Cost will return the total cost (after discounts) in cents.
+Define a function - `cost` - that calculates the cost for a given list of books based on defined discounts.
+
+`cost` will return the total cost (after discounts) in cents.
For example, for a single book, the cost is 800 cents, which equals $8.00.
Only integer calculations are necessary for this exercise.
diff --git a/exercises/practice/book-store/.docs/instructions.md b/exercises/practice/book-store/.docs/instructions.md
index 5d767b0360..54403f17bf 100644
--- a/exercises/practice/book-store/.docs/instructions.md
+++ b/exercises/practice/book-store/.docs/instructions.md
@@ -1,12 +1,10 @@
-# Description
+# Instructions
-To try and encourage more sales of different books from a popular 5 book
-series, a bookshop has decided to offer discounts on multiple book purchases.
+To try and encourage more sales of different books from a popular 5 book series, a bookshop has decided to offer discounts on multiple book purchases.
One copy of any of the five books costs $8.
-If, however, you buy two different books, you get a 5%
-discount on those two books.
+If, however, you buy two different books, you get a 5% discount on those two books.
If you buy 3 different books, you get a 10% discount.
@@ -14,14 +12,9 @@ If you buy 4 different books, you get a 20% discount.
If you buy all 5, you get a 25% discount.
-Note: that if you buy four books, of which 3 are
-different titles, you get a 10% discount on the 3 that
-form part of a set, but the fourth book still costs $8.
+Note that if you buy four books, of which 3 are different titles, you get a 10% discount on the 3 that form part of a set, but the fourth book still costs $8.
-Your mission is to write a piece of code to calculate the
-price of any conceivable shopping basket (containing only
-books of the same series), giving as big a discount as
-possible.
+Your mission is to write code to calculate the price of any conceivable shopping basket (containing only books of the same series), giving as big a discount as possible.
For example, how much does this basket of books cost?
@@ -33,36 +26,36 @@ For example, how much does this basket of books cost?
One way of grouping these 8 books is:
-- 1 group of 5 --> 25% discount (1st,2nd,3rd,4th,5th)
-- +1 group of 3 --> 10% discount (1st,2nd,3rd)
+- 1 group of 5 (1st, 2nd,3rd, 4th, 5th)
+- 1 group of 3 (1st, 2nd, 3rd)
This would give a total of:
- 5 books at a 25% discount
-- +3 books at a 10% discount
+- 3 books at a 10% discount
Resulting in:
-- 5 _ (8 - 2.00) == 5 _ 6.00 == $30.00
-- +3 _ (8 - 0.80) == 3 _ 7.20 == $21.60
+- 5 × (100% - 25%) × $8 = 5 × $6.00 = $30.00, plus
+- 3 × (100% - 10%) × $8 = 3 × $7.20 = $21.60
-For a total of $51.60
+Which equals $51.60.
However, a different way to group these 8 books is:
-- 1 group of 4 books --> 20% discount (1st,2nd,3rd,4th)
-- +1 group of 4 books --> 20% discount (1st,2nd,3rd,5th)
+- 1 group of 4 books (1st, 2nd, 3rd, 4th)
+- 1 group of 4 books (1st, 2nd, 3rd, 5th)
This would give a total of:
- 4 books at a 20% discount
-- +4 books at a 20% discount
+- 4 books at a 20% discount
Resulting in:
-- 4 _ (8 - 1.60) == 4 _ 6.40 == $25.60
-- +4 _ (8 - 1.60) == 4 _ 6.40 == $25.60
+- 4 × (100% - 20%) × $8 = 4 × $6.40 = $25.60, plus
+- 4 × (100% - 20%) × $8 = 4 × $6.40 = $25.60
-For a total of $51.20
+Which equals $51.20.
And $51.20 is the price with the biggest discount.
diff --git a/exercises/practice/book-store/.eslintrc b/exercises/practice/book-store/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/practice/book-store/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/book-store/.gitignore b/exercises/practice/book-store/.gitignore
new file mode 100644
index 0000000000..0c88ff6ec3
--- /dev/null
+++ b/exercises/practice/book-store/.gitignore
@@ -0,0 +1,5 @@
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/practice/book-store/.meta/config.json b/exercises/practice/book-store/.meta/config.json
index 4334c0cd7d..613f425460 100644
--- a/exercises/practice/book-store/.meta/config.json
+++ b/exercises/practice/book-store/.meta/config.json
@@ -15,7 +15,7 @@
},
"blurb": "To try and encourage more sales of different books from a popular 5 book series, a bookshop has decided to offer discounts of multiple-book purchases.",
"source": "Inspired by the harry potter kata from Cyber-Dojo.",
- "source_url": "http://cyber-dojo.org",
+ "source_url": "https://cyber-dojo.org/creator/home",
"custom": {
"version.tests.compatibility": "jest-27",
"flag.tests.task-per-describe": false,
diff --git a/exercises/practice/book-store/.meta/tests.toml b/exercises/practice/book-store/.meta/tests.toml
index 70d3f906a5..8241ee55f2 100644
--- a/exercises/practice/book-store/.meta/tests.toml
+++ b/exercises/practice/book-store/.meta/tests.toml
@@ -1,6 +1,13 @@
-# This is an auto-generated file. Regular comments will be removed when this
-# file is regenerated. Regenerating will not touch any manually added keys,
-# so comments can be added in a "comment" key.
+# 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.
[17146bd5-2e80-4557-ab4c-05632b6b0d01]
description = "Only a single book"
@@ -33,19 +40,27 @@ description = "Two groups of four is cheaper than groups of five and three"
description = "Group of four plus group of two is cheaper than two groups of three"
[68ea9b78-10ad-420e-a766-836a501d3633]
-description = "Two each of first 4 books and 1 copy each of rest"
+description = "Two each of first four books and one copy each of rest"
[c0a779d5-a40c-47ae-9828-a340e936b866]
description = "Two copies of each book"
[18fd86fe-08f1-4b68-969b-392b8af20513]
-description = "Three copies of first book and 2 each of remaining"
+description = "Three copies of first book and two each of remaining"
[0b19a24d-e4cf-4ec8-9db2-8899a41af0da]
-description = "Three each of first 2 books and 2 each of remaining books"
+description = "Three each of first two books and two each of remaining books"
[bb376344-4fb2-49ab-ab85-e38d8354a58d]
description = "Four groups of four are cheaper than two groups each of five and three"
[5260ddde-2703-4915-b45a-e54dbbac4303]
description = "Check that groups of four are created properly even when there are more groups of three than groups of five"
+
+[b0478278-c551-4747-b0fc-7e0be3158b1f]
+description = "One group of one and four is cheaper than one group of two and three"
+include = false
+
+[cf868453-6484-4ae1-9dfc-f8ee85bbde01]
+description = "One group of one and two plus three groups of four is cheaper than one group of each size"
+include = false
diff --git a/exercises/practice/book-store/babel.config.js b/exercises/practice/book-store/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/practice/book-store/babel.config.js
+++ b/exercises/practice/book-store/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/practice/book-store/book-store.js b/exercises/practice/book-store/book-store.js
index b8cc26b5af..11a6fb7c30 100644
--- a/exercises/practice/book-store/book-store.js
+++ b/exercises/practice/book-store/book-store.js
@@ -4,5 +4,5 @@
//
export const cost = (books) => {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
};
diff --git a/exercises/practice/book-store/book-store.spec.js b/exercises/practice/book-store/book-store.spec.js
index 107a6c594e..c0ae7bd61f 100644
--- a/exercises/practice/book-store/book-store.spec.js
+++ b/exercises/practice/book-store/book-store.spec.js
@@ -1,3 +1,4 @@
+import { describe, expect, test, xtest } from '@jest/globals';
import { cost } from './book-store';
describe('Book Store', () => {
diff --git a/exercises/practice/book-store/eslint.config.mjs b/exercises/practice/book-store/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/practice/book-store/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/practice/book-store/jest.config.js b/exercises/practice/book-store/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/practice/book-store/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/practice/book-store/package.json b/exercises/practice/book-store/package.json
index d27ecfe266..c50901f931 100644
--- a/exercises/practice/book-store/package.json
+++ b/exercises/practice/book-store/package.json
@@ -10,20 +10,25 @@
"directory": "exercises/practice/book-store"
},
"devDependencies": {
- "@babel/core": "^7.18.10",
- "@exercism/babel-preset-javascript": "^0.1.2",
- "@exercism/eslint-config-javascript": "^0.6.0",
- "@types/jest": "^28.1.6",
- "@types/node": "^18.6.4",
- "babel-jest": "^28.1.3",
- "core-js": "^3.24.1",
- "eslint": "^8.21.0",
- "jest": "^28.1.3"
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
},
"dependencies": {},
"scripts": {
- "test": "jest ./*",
- "watch": "jest --watch ./*",
- "lint": "eslint ."
- }
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
}
diff --git a/exercises/practice/bottle-song/.docs/instructions.md b/exercises/practice/bottle-song/.docs/instructions.md
new file mode 100644
index 0000000000..febdfc8639
--- /dev/null
+++ b/exercises/practice/bottle-song/.docs/instructions.md
@@ -0,0 +1,57 @@
+# Instructions
+
+Recite the lyrics to that popular children's repetitive song: Ten Green Bottles.
+
+Note that not all verses are identical.
+
+```text
+Ten green bottles hanging on the wall,
+Ten green bottles hanging on the wall,
+And if one green bottle should accidentally fall,
+There'll be nine green bottles hanging on the wall.
+
+Nine green bottles hanging on the wall,
+Nine green bottles hanging on the wall,
+And if one green bottle should accidentally fall,
+There'll be eight green bottles hanging on the wall.
+
+Eight green bottles hanging on the wall,
+Eight green bottles hanging on the wall,
+And if one green bottle should accidentally fall,
+There'll be seven green bottles hanging on the wall.
+
+Seven green bottles hanging on the wall,
+Seven green bottles hanging on the wall,
+And if one green bottle should accidentally fall,
+There'll be six green bottles hanging on the wall.
+
+Six green bottles hanging on the wall,
+Six green bottles hanging on the wall,
+And if one green bottle should accidentally fall,
+There'll be five green bottles hanging on the wall.
+
+Five green bottles hanging on the wall,
+Five green bottles hanging on the wall,
+And if one green bottle should accidentally fall,
+There'll be four green bottles hanging on the wall.
+
+Four green bottles hanging on the wall,
+Four green bottles hanging on the wall,
+And if one green bottle should accidentally fall,
+There'll be three green bottles hanging on the wall.
+
+Three green bottles hanging on the wall,
+Three green bottles hanging on the wall,
+And if one green bottle should accidentally fall,
+There'll be two green bottles hanging on the wall.
+
+Two green bottles hanging on the wall,
+Two green bottles hanging on the wall,
+And if one green bottle should accidentally fall,
+There'll be one green bottle hanging on the wall.
+
+One green bottle hanging on the wall,
+One green bottle hanging on the wall,
+And if one green bottle should accidentally fall,
+There'll be no green bottles hanging on the wall.
+```
diff --git a/exercises/practice/bottle-song/.gitignore b/exercises/practice/bottle-song/.gitignore
new file mode 100644
index 0000000000..0c88ff6ec3
--- /dev/null
+++ b/exercises/practice/bottle-song/.gitignore
@@ -0,0 +1,5 @@
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/practice/bottle-song/.meta/config.json b/exercises/practice/bottle-song/.meta/config.json
new file mode 100644
index 0000000000..db4f3be94c
--- /dev/null
+++ b/exercises/practice/bottle-song/.meta/config.json
@@ -0,0 +1,19 @@
+{
+ "authors": [
+ "Cool-Katt"
+ ],
+ "files": {
+ "solution": [
+ "bottle-song.js"
+ ],
+ "test": [
+ "bottle-song.spec.js"
+ ],
+ "example": [
+ ".meta/proof.ci.js"
+ ]
+ },
+ "blurb": "Produce the lyrics to the popular children's repetitive song: Ten Green Bottles.",
+ "source": "Wikipedia",
+ "source_url": "https://en.wikipedia.org/wiki/Ten_Green_Bottles"
+}
diff --git a/exercises/practice/bottle-song/.meta/proof.ci.js b/exercises/practice/bottle-song/.meta/proof.ci.js
new file mode 100644
index 0000000000..81f8a4df42
--- /dev/null
+++ b/exercises/practice/bottle-song/.meta/proof.ci.js
@@ -0,0 +1,38 @@
+const NUMBERS = {
+ 10: 'ten',
+ 9: 'nine',
+ 8: 'eight',
+ 7: 'seven',
+ 6: 'six',
+ 5: 'five',
+ 4: 'four',
+ 3: 'three',
+ 2: 'two',
+ 1: 'one',
+ 0: 'no',
+};
+
+export const recite = (initialBottlesCount, takeDownCount = 1) => {
+ let out = [];
+ for (
+ let i = initialBottlesCount;
+ i > initialBottlesCount - takeDownCount;
+ i--
+ ) {
+ let plural = i === 1 ? '' : 's';
+ let secondPlural = i - 1 === 1 ? '' : 's';
+ out.push(
+ textsTemplate(cap(NUMBERS[i]), NUMBERS[i - 1], plural, secondPlural),
+ );
+ if (initialBottlesCount - takeDownCount !== i - 1) out.push('');
+ }
+ return out.flat();
+};
+
+const textsTemplate = (curCount, prevCount, pluralOne, pluralTwo) => [
+ ...Array(2).fill(`${curCount} green bottle${pluralOne} hanging on the wall,`),
+ `And if one green bottle should accidentally fall,`,
+ `There'll be ${prevCount} green bottle${pluralTwo} hanging on the wall.`,
+];
+
+const cap = (str) => str.replace(/./i, (char) => char.toUpperCase());
diff --git a/exercises/practice/bottle-song/.meta/tests.toml b/exercises/practice/bottle-song/.meta/tests.toml
new file mode 100644
index 0000000000..1f6e40a37c
--- /dev/null
+++ b/exercises/practice/bottle-song/.meta/tests.toml
@@ -0,0 +1,31 @@
+# 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.
+
+[d4ccf8fc-01dc-48c0-a201-4fbeb30f2d03]
+description = "verse -> single verse -> first generic verse"
+
+[0f0aded3-472a-4c64-b842-18d4f1f5f030]
+description = "verse -> single verse -> last generic verse"
+
+[f61f3c97-131f-459e-b40a-7428f3ed99d9]
+description = "verse -> single verse -> verse with 2 bottles"
+
+[05eadba9-5dbd-401e-a7e8-d17cc9baa8e0]
+description = "verse -> single verse -> verse with 1 bottle"
+
+[a4a28170-83d6-4dc1-bd8b-319b6abb6a80]
+description = "lyrics -> multiple verses -> first two verses"
+
+[3185d438-c5ac-4ce6-bcd3-02c9ff1ed8db]
+description = "lyrics -> multiple verses -> last three verses"
+
+[28c1584a-0e51-4b65-9ae2-fbc0bf4bbb28]
+description = "lyrics -> multiple verses -> all verses"
diff --git a/exercises/practice/bottle-song/.npmrc b/exercises/practice/bottle-song/.npmrc
new file mode 100644
index 0000000000..d26df800bb
--- /dev/null
+++ b/exercises/practice/bottle-song/.npmrc
@@ -0,0 +1 @@
+audit=false
diff --git a/exercises/practice/bottle-song/LICENSE b/exercises/practice/bottle-song/LICENSE
new file mode 100644
index 0000000000..90e73be03b
--- /dev/null
+++ b/exercises/practice/bottle-song/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/bottle-song/babel.config.js b/exercises/practice/bottle-song/babel.config.js
new file mode 100644
index 0000000000..a638497df1
--- /dev/null
+++ b/exercises/practice/bottle-song/babel.config.js
@@ -0,0 +1,4 @@
+module.exports = {
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
+ plugins: [],
+};
diff --git a/exercises/practice/bottle-song/bottle-song.js b/exercises/practice/bottle-song/bottle-song.js
new file mode 100644
index 0000000000..4d05f10d57
--- /dev/null
+++ b/exercises/practice/bottle-song/bottle-song.js
@@ -0,0 +1,8 @@
+//
+// This is only a SKELETON file for the 'Bottle Song' exercise. It's been provided as a
+// convenience to get you started writing code faster.
+//
+
+export const recite = (initialBottlesCount, takeDownCount) => {
+ throw new Error('Remove this line and implement the function');
+};
diff --git a/exercises/practice/bottle-song/bottle-song.spec.js b/exercises/practice/bottle-song/bottle-song.spec.js
new file mode 100644
index 0000000000..75cde11eec
--- /dev/null
+++ b/exercises/practice/bottle-song/bottle-song.spec.js
@@ -0,0 +1,142 @@
+import { describe, expect, test, xtest } from '@jest/globals';
+import { recite } from './bottle-song';
+
+describe('Bottle Song', () => {
+ describe('verse', () => {
+ describe('single verse', () => {
+ test('first generic verse', () => {
+ let expected = [
+ `Ten green bottles hanging on the wall,`,
+ `Ten green bottles hanging on the wall,`,
+ `And if one green bottle should accidentally fall,`,
+ `There'll be nine green bottles hanging on the wall.`,
+ ];
+ expect(recite(10, 1)).toEqual(expected);
+ });
+
+ xtest('last generic verse', () => {
+ let expected = [
+ `Three green bottles hanging on the wall,`,
+ `Three green bottles hanging on the wall,`,
+ `And if one green bottle should accidentally fall,`,
+ `There'll be two green bottles hanging on the wall.`,
+ ];
+ expect(recite(3, 1)).toEqual(expected);
+ });
+
+ xtest('verse with 2 bottles', () => {
+ let expected = [
+ `Two green bottles hanging on the wall,`,
+ `Two green bottles hanging on the wall,`,
+ `And if one green bottle should accidentally fall,`,
+ `There'll be one green bottle hanging on the wall.`,
+ ];
+ expect(recite(2, 1)).toEqual(expected);
+ });
+
+ xtest('verse with 1 bottle', () => {
+ let expected = [
+ `One green bottle hanging on the wall,`,
+ `One green bottle hanging on the wall,`,
+ `And if one green bottle should accidentally fall,`,
+ `There'll be no green bottles hanging on the wall.`,
+ ];
+ expect(recite(1, 1)).toEqual(expected);
+ });
+ });
+ });
+
+ describe('lyrics', () => {
+ describe('multiple verses', () => {
+ xtest('first two verses', () => {
+ let expected = [
+ `Ten green bottles hanging on the wall,`,
+ `Ten green bottles hanging on the wall,`,
+ `And if one green bottle should accidentally fall,`,
+ `There'll be nine green bottles hanging on the wall.`,
+ ``,
+ `Nine green bottles hanging on the wall,`,
+ `Nine green bottles hanging on the wall,`,
+ `And if one green bottle should accidentally fall,`,
+ `There'll be eight green bottles hanging on the wall.`,
+ ];
+ expect(recite(10, 2)).toEqual(expected);
+ });
+
+ xtest('last three verses', () => {
+ let expected = [
+ `Three green bottles hanging on the wall,`,
+ `Three green bottles hanging on the wall,`,
+ `And if one green bottle should accidentally fall,`,
+ `There'll be two green bottles hanging on the wall.`,
+ ``,
+ `Two green bottles hanging on the wall,`,
+ `Two green bottles hanging on the wall,`,
+ `And if one green bottle should accidentally fall,`,
+ `There'll be one green bottle hanging on the wall.`,
+ ``,
+ `One green bottle hanging on the wall,`,
+ `One green bottle hanging on the wall,`,
+ `And if one green bottle should accidentally fall,`,
+ `There'll be no green bottles hanging on the wall.`,
+ ];
+ expect(recite(3, 3)).toEqual(expected);
+ });
+
+ xtest('all verses', () => {
+ let expected = [
+ `Ten green bottles hanging on the wall,`,
+ `Ten green bottles hanging on the wall,`,
+ `And if one green bottle should accidentally fall,`,
+ `There'll be nine green bottles hanging on the wall.`,
+ ``,
+ `Nine green bottles hanging on the wall,`,
+ `Nine green bottles hanging on the wall,`,
+ `And if one green bottle should accidentally fall,`,
+ `There'll be eight green bottles hanging on the wall.`,
+ ``,
+ `Eight green bottles hanging on the wall,`,
+ `Eight green bottles hanging on the wall,`,
+ `And if one green bottle should accidentally fall,`,
+ `There'll be seven green bottles hanging on the wall.`,
+ ``,
+ `Seven green bottles hanging on the wall,`,
+ `Seven green bottles hanging on the wall,`,
+ `And if one green bottle should accidentally fall,`,
+ `There'll be six green bottles hanging on the wall.`,
+ ``,
+ `Six green bottles hanging on the wall,`,
+ `Six green bottles hanging on the wall,`,
+ `And if one green bottle should accidentally fall,`,
+ `There'll be five green bottles hanging on the wall.`,
+ ``,
+ `Five green bottles hanging on the wall,`,
+ `Five green bottles hanging on the wall,`,
+ `And if one green bottle should accidentally fall,`,
+ `There'll be four green bottles hanging on the wall.`,
+ ``,
+ `Four green bottles hanging on the wall,`,
+ `Four green bottles hanging on the wall,`,
+ `And if one green bottle should accidentally fall,`,
+ `There'll be three green bottles hanging on the wall.`,
+ ``,
+ `Three green bottles hanging on the wall,`,
+ `Three green bottles hanging on the wall,`,
+ `And if one green bottle should accidentally fall,`,
+ `There'll be two green bottles hanging on the wall.`,
+ ``,
+ `Two green bottles hanging on the wall,`,
+ `Two green bottles hanging on the wall,`,
+ `And if one green bottle should accidentally fall,`,
+ `There'll be one green bottle hanging on the wall.`,
+ ``,
+ `One green bottle hanging on the wall,`,
+ `One green bottle hanging on the wall,`,
+ `And if one green bottle should accidentally fall,`,
+ `There'll be no green bottles hanging on the wall.`,
+ ];
+ expect(recite(10, 10)).toEqual(expected);
+ });
+ });
+ });
+});
diff --git a/exercises/practice/bottle-song/eslint.config.mjs b/exercises/practice/bottle-song/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/practice/bottle-song/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/practice/bottle-song/jest.config.js b/exercises/practice/bottle-song/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/practice/bottle-song/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '/test/fixtures',
+ '/test/helpers',
+ '__mocks__',
+ ],
+ transform: {
+ '^.+\\.[jt]sx?$': 'babel-jest',
+ },
+ moduleNameMapper: {
+ '^(\\.\\/.+)\\.js$': '$1',
+ },
+};
diff --git a/exercises/practice/bottle-song/package.json b/exercises/practice/bottle-song/package.json
new file mode 100644
index 0000000000..daf9c133b8
--- /dev/null
+++ b/exercises/practice/bottle-song/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "@exercism/javascript-bottle-song",
+ "description": "Exercism practice exercise on bottle-song",
+ "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/bottle-song"
+ },
+ "devDependencies": {
+ "@exercism/babel-preset-javascript": "^0.5.1",
+ "@exercism/eslint-config-javascript": "^0.8.1",
+ "@jest/globals": "^30.3.0",
+ "@types/node": "^24.3.0",
+ "@types/shelljs": "^0.8.17",
+ "babel-jest": "^30.3.0",
+ "core-js": "~3.46.0",
+ "diff": "^8.0.3",
+ "eslint": "^9.28.0",
+ "expect": "^29.7.0",
+ "globals": "^16.3.0",
+ "jest": "^30.2.0"
+ },
+ "dependencies": {},
+ "scripts": {
+ "lint": "corepack pnpm eslint .",
+ "test": "corepack pnpm jest",
+ "watch": "corepack pnpm jest --watch",
+ "format": "corepack pnpm prettier -w ."
+ },
+ "packageManager": "pnpm@9.15.2"
+}
diff --git a/exercises/practice/bowling/.docs/instructions.md b/exercises/practice/bowling/.docs/instructions.md
index 6c6575a1b2..60ccad1b61 100644
--- a/exercises/practice/bowling/.docs/instructions.md
+++ b/exercises/practice/bowling/.docs/instructions.md
@@ -2,29 +2,24 @@
Score a bowling game.
-Bowling is a game where players roll a heavy ball to knock down pins
-arranged in a triangle. Write code to keep track of the score
-of a game of bowling.
+Bowling is a game where players roll a heavy ball to knock down pins arranged in a triangle.
+Write code to keep track of the score of a game of bowling.
## Scoring Bowling
-The game consists of 10 frames. A frame is composed of one or two ball
-throws with 10 pins standing at frame initialization. There are three
-cases for the tabulation of a frame.
+The game consists of 10 frames.
+A frame is composed of one or two ball throws with 10 pins standing at frame initialization.
+There are three cases for the tabulation of a frame.
-- An open frame is where a score of less than 10 is recorded for the
- frame. In this case the score for the frame is the number of pins
- knocked down.
+- An open frame is where a score of less than 10 is recorded for the frame.
+ In this case the score for the frame is the number of pins knocked down.
-- A spare is where all ten pins are knocked down by the second
- throw. The total value of a spare is 10 plus the number of pins
- knocked down in their next throw.
+- A spare is where all ten pins are knocked down by the second throw.
+ The total value of a spare is 10 plus the number of pins knocked down in their next throw.
-- A strike is where all ten pins are knocked down by the first
- throw. The total value of a strike is 10 plus the number of pins
- knocked down in the next two throws. If a strike is immediately
- followed by a second strike, then the value of the first strike
- cannot be determined until the ball is thrown one more time.
+- A strike is where all ten pins are knocked down by the first throw.
+ The total value of a strike is 10 plus the number of pins knocked down in the next two throws.
+ If a strike is immediately followed by a second strike, then the value of the first strike cannot be determined until the ball is thrown one more time.
Here is a three frame example:
@@ -40,11 +35,11 @@ Frame 3 is (9 + 0) = 9
This means the current running total is 48.
-The tenth frame in the game is a special case. If someone throws a
-strike or a spare then they get a fill ball. Fill balls exist to
-calculate the total of the 10th frame. Scoring a strike or spare on
-the fill ball does not give the player more fill balls. The total
-value of the 10th frame is the total number of pins knocked down.
+The tenth frame in the game is a special case.
+If someone throws a spare or a strike then they get one or two fill balls respectively.
+Fill balls exist to calculate the total of the 10th frame.
+Scoring a strike or spare on the fill ball does not give the player more fill balls.
+The total value of the 10th frame is the total number of pins knocked down.
For a tenth frame of X1/ (strike and a spare), the total value is 20.
@@ -52,10 +47,10 @@ For a tenth frame of XXX (three strikes), the total value is 30.
## Requirements
-Write code to keep track of the score of a game of bowling. It should
-support two operations:
+Write code to keep track of the score of a game of bowling.
+It should support two operations:
-- `roll(pins : int)` is called each time the player rolls a ball. The
- argument is the number of pins knocked down.
-- `score() : int` is called only at the very end of the game. It
- returns the total score for that game.
+- `roll(pins : int)` is called each time the player rolls a ball.
+ The argument is the number of pins knocked down.
+- `score() : int` is called only at the very end of the game.
+ It returns the total score for that game.
diff --git a/exercises/practice/bowling/.eslintrc b/exercises/practice/bowling/.eslintrc
deleted file mode 100644
index 1d4446029c..0000000000
--- a/exercises/practice/bowling/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "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/bowling/.gitignore b/exercises/practice/bowling/.gitignore
new file mode 100644
index 0000000000..0c88ff6ec3
--- /dev/null
+++ b/exercises/practice/bowling/.gitignore
@@ -0,0 +1,5 @@
+/node_modules
+/bin/configlet
+/bin/configlet.exe
+/package-lock.json
+/yarn.lock
diff --git a/exercises/practice/bowling/.meta/config.json b/exercises/practice/bowling/.meta/config.json
index cd8bca6e9c..4d1535b428 100644
--- a/exercises/practice/bowling/.meta/config.json
+++ b/exercises/practice/bowling/.meta/config.json
@@ -4,6 +4,7 @@
],
"contributors": [
"danielj-jordan",
+ "jagdish-15",
"SleeplessByte",
"tejasbubane"
],
@@ -18,9 +19,9 @@
".meta/proof.ci.js"
]
},
- "blurb": "Score a bowling game",
- "source": "The Bowling Game Kata at but UncleBob",
- "source_url": "http://butunclebob.com/ArticleS.UncleBob.TheBowlingGameKata",
+ "blurb": "Score a bowling game.",
+ "source": "The Bowling Game Kata from UncleBob",
+ "source_url": "https://web.archive.org/web/20221001111000/http://butunclebob.com/ArticleS.UncleBob.TheBowlingGameKata",
"custom": {
"version.tests.compatibility": "jest-27",
"flag.tests.task-per-describe": false,
diff --git a/exercises/practice/bowling/.meta/tests.toml b/exercises/practice/bowling/.meta/tests.toml
index 963df175a7..19042607d7 100644
--- a/exercises/practice/bowling/.meta/tests.toml
+++ b/exercises/practice/bowling/.meta/tests.toml
@@ -1,6 +1,13 @@
-# This is an auto-generated file. Regular comments will be removed when this
-# file is regenerated. Regenerating will not touch any manually added keys,
-# so comments can be added in a "comment" key.
+# 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.
[656ae006-25c2-438c-a549-f338e7ec7441]
description = "should be able to score a game with all zeros"
@@ -38,6 +45,9 @@ description = "rolling a spare with the two roll bonus does not get a bonus roll
[576faac1-7cff-4029-ad72-c16bcada79b5]
description = "strikes with the two roll bonus do not get bonus rolls"
+[efb426ec-7e15-42e6-9b96-b4fca3ec2359]
+description = "last two strikes followed by only last bonus with non strike points"
+
[72e24404-b6c6-46af-b188-875514c0377b]
description = "a strike with the one roll bonus after a spare in the last frame does not get a bonus"
diff --git a/exercises/practice/bowling/babel.config.js b/exercises/practice/bowling/babel.config.js
index b781d5a667..a638497df1 100644
--- a/exercises/practice/bowling/babel.config.js
+++ b/exercises/practice/bowling/babel.config.js
@@ -1,4 +1,4 @@
module.exports = {
- presets: ['@exercism/babel-preset-javascript'],
+ presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]],
plugins: [],
};
diff --git a/exercises/practice/bowling/bowling.js b/exercises/practice/bowling/bowling.js
index 9d83338b7c..d3b490db36 100644
--- a/exercises/practice/bowling/bowling.js
+++ b/exercises/practice/bowling/bowling.js
@@ -5,10 +5,10 @@
export class Bowling {
roll() {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
}
score() {
- throw new Error('Remove this statement and implement this function');
+ throw new Error('Remove this line and implement the function');
}
}
diff --git a/exercises/practice/bowling/bowling.spec.js b/exercises/practice/bowling/bowling.spec.js
index 981471cbf6..6e95f7c5f6 100644
--- a/exercises/practice/bowling/bowling.spec.js
+++ b/exercises/practice/bowling/bowling.spec.js
@@ -1,3 +1,4 @@
+import { describe, expect, test, xtest } from '@jest/globals';
import { Bowling } from './bowling';
describe('Bowling', () => {
@@ -128,6 +129,17 @@ describe('Bowling', () => {
expect(bowling.score()).toEqual(30);
});
+ xtest('last two strikes followed by only last bonus with non strike points', () => {
+ const rolls = [
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 0, 1,
+ ];
+ const bowling = new Bowling();
+ rolls.forEach((roll) => {
+ bowling.roll(roll);
+ });
+ expect(bowling.score()).toEqual(31);
+ });
+
xtest('a strike with the one roll bonus after a spare in the last frame does not get a bonus', () => {
const rolls = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3, 10,
diff --git a/exercises/practice/bowling/eslint.config.mjs b/exercises/practice/bowling/eslint.config.mjs
new file mode 100644
index 0000000000..ca517111ed
--- /dev/null
+++ b/exercises/practice/bowling/eslint.config.mjs
@@ -0,0 +1,45 @@
+// @ts-check
+
+import config from '@exercism/eslint-config-javascript';
+import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs';
+
+import globals from 'globals';
+
+export default [
+ ...config,
+ ...maintainersConfig,
+ {
+ files: maintainersConfig[1].files,
+ rules: {
+ 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }],
+ },
+ },
+ {
+ files: ['scripts/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ },
+ // <>
+ {
+ ignores: [
+ // # Protected or generated
+ '/.appends/**/*',
+ '/.github/**/*',
+ '/.vscode/**/*',
+
+ // # Binaries
+ '/bin/*',
+
+ // # Configuration
+ '/config',
+ '/babel.config.js',
+
+ // # Typings
+ '/exercises/**/global.d.ts',
+ '/exercises/**/env.d.ts',
+ ],
+ },
+];
diff --git a/exercises/practice/bowling/jest.config.js b/exercises/practice/bowling/jest.config.js
new file mode 100644
index 0000000000..ec8e908127
--- /dev/null
+++ b/exercises/practice/bowling/jest.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ verbose: true,
+ projects: [''],
+ testMatch: [
+ '**/__tests__/**/*.[jt]s?(x)',
+ '**/test/**/*.[jt]s?(x)',
+ '**/?(*.)+(spec|test).[jt]s?(x)',
+ ],
+ testPathIgnorePatterns: [
+ '/(?:production_)?node_modules/',
+ '.d.ts$',
+ '