diff --git a/bin/add-exercise b/bin/add-exercise new file mode 100755 index 00000000..025184b5 --- /dev/null +++ b/bin/add-exercise @@ -0,0 +1,103 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if (( $# != 1 )); then + echo >&2 "Populates a new directory for a practice exercise." + echo >&2 "Usage: $0 " + exit 1 +fi + +die() { echo >&2 "$*"; exit 1; } + +required_tool() { + command -v "$1" >/dev/null 2>&1 || + die "$1 is required but not installed. Please install it and make sure it's in your PATH." +} +required_tool jq +required_tool curl + +[[ -f ./bin/fetch-configlet ]] || die "run this script from the repo's root directory." + + +slug="${1}" +name=$(echo "${slug}" | sed 's/\b\w/\u&/g; s/-/ /g') # assumes GNU sed + +it_exists=$( + jq --arg slug "${slug}" ' + .exercises.practice + | map(select(.slug == $slug)) + | length > 0 + ' config.json +) +[[ ${it_exists} == false ]] || die "${slug} already exists in config.json" + +# Add entry for exercise in config.json +./bin/fetch-configlet +jq --arg slug "${slug}" \ + --arg uuid "$(./bin/configlet uuid)" \ + --arg name "${name}" \ + ' + .exercises.practice += [ + { + slug: $slug, + name: $name, + uuid: $uuid, + practices: [], + prerequisites: [], + difficulty: 1 + } + ] + ' config.json > config.json.tmp \ +&& mv config.json.tmp config.json + +# Sync the exercise +./bin/configlet sync --update --yes \ + --tests include \ + --metadata \ + --docs \ + --exercise "${slug}" + +cp lib/test-words.8th "exercises/practice/${slug}" + +cat << END_TEST > "exercises/practice/${slug}/test.8th" +"test-words.8th" dup . cr f:include +"${slug}.8th" dup . cr f:include +"${slug}-tests.8th" dup . cr f:include +bye +END_TEST + +touch "exercises/practice/${slug}/${slug}.8th" +touch "exercises/practice/${slug}/.meta/example.8th" +curl --silent "https://raw.githubusercontent.com/exercism/problem-specifications/main/exercises/${slug}/canonical-data.json" > "exercises/practice/${slug}/${slug}-tests.8th" + +echo +read -rp 'Your github username: ' author +conf="exercises/practice/${slug}/.meta/config.json" +jq --arg slug "${slug}" \ + --arg author "${author}" \ + ' + .authors = [$author] | + .files = { + solution: [$slug + ".8th"], + test: [$slug + "-tests.8th"], + example: [".meta/example.8th"] + } + ' "${conf}" > "${conf}.tmp" \ +&& mv "${conf}.tmp" "${conf}" + +echo +ls -laR "exercises/practice/${slug}" + +cat << NEXT_STEPS + +Your next steps are: +- Create the test suite in 'exercises/practice/${slug}/${slug}-tests.8th' + based on the canonical data at 'https://github.com/exercism/problem-specifications/blob/main/exercises/${slug}/canonical-data.json' + - Any test cases you don't implement, mark them in 'exercises/practice/${slug}/.meta/tests.toml' with "include = false" +- Create the example solution in 'exercises/practice/${slug}/.meta/example.8th' +- Verify the example solution by running 'bin/test-no-docker ${slug}' +- Create the stub solution in 'exercises/practice/${slug}/${slug}.8th' +- Update the 'difficulty' value for the exercise's entry in the 'config.json' file +- Validate CI using 'bin/configlet lint' +NEXT_STEPS diff --git a/config.json b/config.json index 4e848df7..901bfc4f 100644 --- a/config.json +++ b/config.json @@ -234,6 +234,14 @@ "prerequisites": [], "difficulty": 9, "practices": [] + }, + { + "slug": "sieve", + "name": "Sieve", + "uuid": "e31eac0d-cece-440e-a2be-1431e36284bf", + "practices": [], + "prerequisites": [], + "difficulty": 8 } ] }, diff --git a/exercises/practice/sieve/.docs/instructions.md b/exercises/practice/sieve/.docs/instructions.md new file mode 100644 index 00000000..3adf1d55 --- /dev/null +++ b/exercises/practice/sieve/.docs/instructions.md @@ -0,0 +1,28 @@ +# Instructions + +Your task is to create a program that implements the Sieve of Eratosthenes algorithm to find prime numbers. + +A prime number is a number that is only divisible by 1 and itself. +For example, 2, 3, 5, 7, 11, and 13 are prime numbers. + +The Sieve of Eratosthenes is an ancient algorithm that works by taking a list of numbers and crossing out all the numbers that aren't prime. + +A number that is **not** prime is called a "composite number". + +To use the Sieve of Eratosthenes, you first create a list of all the numbers between 2 and your given number. +Then you repeat the following steps: + +1. Find the next unmarked number in your list. This is a prime number. +2. Mark all the multiples of that prime number as composite (not prime). + +You keep repeating these steps until you've gone through every number in your list. +At the end, all the unmarked numbers are prime. + +~~~~exercism/note +[Wikipedia's Sieve of Eratosthenes article][eratosthenes] has a useful graphic that explains the algorithm. + +The tests don't check that you've implemented the algorithm, only that you've come up with the correct list of primes. +A good first test is to check that you do not use division or remainder operations. + +[eratosthenes]: https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes +~~~~ diff --git a/exercises/practice/sieve/.docs/introduction.md b/exercises/practice/sieve/.docs/introduction.md new file mode 100644 index 00000000..f6c1cf79 --- /dev/null +++ b/exercises/practice/sieve/.docs/introduction.md @@ -0,0 +1,7 @@ +# Introduction + +You bought a big box of random computer parts at a garage sale. +You've started putting the parts together to build custom computers. + +You want to test the performance of different combinations of parts, and decide to create your own benchmarking program to see how your computers compare. +You choose the famous "Sieve of Eratosthenes" algorithm, an ancient algorithm, but one that should push your computers to the limits. diff --git a/exercises/practice/sieve/.meta/config.json b/exercises/practice/sieve/.meta/config.json new file mode 100644 index 00000000..c9e5a2d8 --- /dev/null +++ b/exercises/practice/sieve/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "glennj" + ], + "files": { + "solution": [ + "sieve.8th" + ], + "test": [ + "sieve-tests.8th" + ], + "example": [ + ".meta/example.8th" + ] + }, + "blurb": "Use the Sieve of Eratosthenes to find all the primes from 2 up to a given number.", + "source": "Sieve of Eratosthenes at Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes" +} diff --git a/exercises/practice/sieve/.meta/example.8th b/exercises/practice/sieve/.meta/example.8th new file mode 100644 index 00000000..dfac11d1 --- /dev/null +++ b/exercises/practice/sieve/.meta/example.8th @@ -0,0 +1,53 @@ +(* from https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes#Algorithm_and_variants + * + * algorithm Sieve of Eratosthenes is + * input: an integer n > 1. + * output: all prime numbers from 2 through n. + * + * let A be an array of Boolean values, indexed by integers 2 to n, + * initially all set to true. + * + * for i = 2, 3, 4, ..., not exceeding √n do + * if A[i] is true + * for j = i2, i2+i, i2+2i, i2+3i, ..., not exceeding n do + * set A[j] := false + * + * return all i such that A[i] is true. + *) + +: make-flag-array \ -- a + ( drop true ) 0 r@ a:generate + 0 false a:! + 1 false a:! +; + +: mark-multiples-of \ a i -- a + 2dup a:@ !if 2drop ;; then + drop + dup dup n:* \ a i idx (initial idx is i^2) + repeat + third over \ a i idx a idx + false a:! \ a i idx a + drop over n:+ + dup r@ n:> + until! + 2drop +; + +: mark-multiples \ a -- a + ' mark-multiples-of 2 r@ n:sqrt loop +; + +: extract-primes \ a1 -- a2 + a:new >r + ( if r> a:push >r else drop then ) a:each + drop r> +; + +: primes \ n -- a + >r + make-flag-array + mark-multiples + extract-primes + rdrop +; diff --git a/exercises/practice/sieve/.meta/tests.toml b/exercises/practice/sieve/.meta/tests.toml new file mode 100644 index 00000000..fec5e1a1 --- /dev/null +++ b/exercises/practice/sieve/.meta/tests.toml @@ -0,0 +1,25 @@ +# 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. + +[88529125-c4ce-43cc-bb36-1eb4ddd7b44f] +description = "no primes under two" + +[4afe9474-c705-4477-9923-840e1024cc2b] +description = "find first prime" + +[974945d8-8cd9-4f00-9463-7d813c7f17b7] +description = "find primes up to 10" + +[2e2417b7-3f3a-452a-8594-b9af08af6d82] +description = "limit is prime" + +[92102a05-4c7c-47de-9ed0-b7d5fcd00f21] +description = "find primes up to 1000" diff --git a/exercises/practice/sieve/sieve-tests.8th b/exercises/practice/sieve/sieve-tests.8th new file mode 100644 index 00000000..583e2498 --- /dev/null +++ b/exercises/practice/sieve/sieve-tests.8th @@ -0,0 +1,40 @@ +5 tests + +"no primes under two" + [] + ( 1 primes ) + test_array_eq_num + +SKIP-REST-OF-TESTS + +"find first prime" + [2] + ( 2 primes ) + test_array_eq_num + +"find primes up to 10" + [2, 3, 5, 7] + ( 10 primes ) + test_array_eq_num + +"limit is prime" + [2, 3, 5, 7, 11, 13] + ( 13 primes ) + test_array_eq_num + +"find primes up to 1000" + [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, + 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, + 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, + 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, + 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, + 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, + 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, + 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, + 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, + 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, + 947, 953, 967, 971, 977, 983, 991, 997] + ( 1000 primes ) + test_array_eq_num + +end-of-tests diff --git a/exercises/practice/sieve/sieve.8th b/exercises/practice/sieve/sieve.8th new file mode 100644 index 00000000..0e2576c8 --- /dev/null +++ b/exercises/practice/sieve/sieve.8th @@ -0,0 +1,2 @@ +: primes \ n -- a +; diff --git a/exercises/practice/sieve/test-words.8th b/exercises/practice/sieve/test-words.8th new file mode 100644 index 00000000..39cf1144 --- /dev/null +++ b/exercises/practice/sieve/test-words.8th @@ -0,0 +1,168 @@ +needs console/loaded + +-1 var, test-count +var tests-passed +var tests-failed +var tests-skipped + +true var, run-test +: SKIP-REST-OF-TESTS false run-test ! ; + +: tests \ n -- + test-count ! +; + +: test-passed \ s -- + 1 tests-passed n:+! + con:green con:onBlack . space " ... OK" . con:white con:onBlack cr +; + +: test-skipped \ s -- + 1 tests-skipped n:+! + con:cyan con:onBlack . space " ... SKIPPED" . con:white con:onBlack cr +; + +: test-failed \ s -- + 1 tests-failed n:+! + con:red con:onBlack . space " ... FAIL" . con:white con:onBlack cr +; + +: isword? \ x -- x f + dup >kind ns:w n:= +; + +: run-test? \ -- T + run-test @ if true else "RUN_ALL_TESTS" getenv n:>bool then +; + +: test_eq \ s x w -- | s w x -- + run-test? !if 2drop test-skipped ;; then + isword? !if swap then + w:exec + n:= + if test-passed else test-failed then +; + +: test_eqs \ s x w -- | s w x -- + run-test? !if 2drop test-skipped ;; then + isword? !if swap then + w:exec + s:= + if test-passed else test-failed then +; + +: test_true \ s w -- + run-test? !if drop test-skipped ;; then + w:exec + if test-passed else test-failed then +; + +: test_false \ s w -- + run-test? !if drop test-skipped ;; then + w:exec + if test-failed else test-passed then +; + +: test_null \ s w -- + run-test? !if drop test-skipped ;; then + w:exec + null? nip + if test-passed else test-failed then +; + +\ compare arrays by testing elements with string equality +: test_eqa \ s x w -- | s w x -- + run-test? !if 2drop test-skipped ;; then + isword? !if swap then + w:exec + ' s:= a:= 2nip + if test-passed else test-failed then +; + +\ Compare a1 to a2. Individual elements are compared with w (e.g., n:cmp). +\ The result n is: +\ - The first non-zero result of ( a1[i] a2[i] w ), or +\ - n:cmp of the lengths of a1 and a2 +\ Note: This may be used in some test files. For example, to compare results +\ that are not required to be in a certain order. +: a:cmp SED: a1 a2 w -- n + >r + over a:len nip over a:len nip n:cmp + true mark -rot \ Stack: length-cmp a1 a2 + ( r@ w:exec nip dup if break else drop then ) a:2each + rdrop \ Done with comparison word + 2drop \ Done with a1 and a2 + mark? + !if \ Got a non-zero result from a compare + nip + then +; + +\ compare arrays by testing elements with numeric equality +: test_array_eq_num \ s x w -- | s w x -- + run-test? !if 2drop test-skipped ;; then + isword? !if swap then + w:exec + ' n:cmp a:cmp 0 n:= + if test-passed else test-failed then +; + +\ Test that array a is equal to the result of word w. Compare arrays by +\ testing elements with array equality. The SED of w is -- a1, where a1 +\ is an array of arrays. The elements of each sub-array must be numbers. +: test_eqa2 SED: s a w -- + run-test? dup . cr !if 2drop test-skipped ;; then + isword? !if swap then + w:exec + .s + ( ' n:= a:= nip nip ) a:= nip nip + if test-passed else test-failed then +; + +: test_map_eq \ s m w -- | s w m -- + run-test? !if 2drop test-skipped ;; then + isword? !if swap then + w:exec + ' n:= m:= 2nip + if test-passed else test-failed then +; + +: test_map_neq \ s m w -- | s w m -- + run-test? !if 2drop test-skipped ;; then + isword? !if swap then + w:exec + ' n:= m:= 2nip + if test-failed else test-passed then +; + +\ Num passed + num skipped + num failed should == num tests +: all-tests-run? \ -- T + tests-passed @ tests-skipped @ tests-failed @ n:+ n:+ + test-count @ n:= +; + +( all-tests-run? + !if con:red con:onBlack "... FAIL - not all tests completed" . con:white con:onBlack cr then +) onexit + +\ Print a summary of the tests run +( con:white con:onBlack + test-count @ . space "tests planned - " . + tests-passed @ . space "passed - " . + tests-skipped @ . space "skipped - " . + tests-failed @ . space "failed" . cr +) onexit + +\ Set the exit status: +\ 0 = all OK +\ 1 = not all tests were run (some error occurred) +\ 2 = some tests failed +: end-of-tests \ -- + all-tests-run? + if + tests-failed @ 0 n:= if 0 else 2 then + else + 1 + then + die +; diff --git a/exercises/practice/sieve/test.8th b/exercises/practice/sieve/test.8th new file mode 100644 index 00000000..99acad3a --- /dev/null +++ b/exercises/practice/sieve/test.8th @@ -0,0 +1,4 @@ +"test-words.8th" dup . cr f:include +"sieve.8th" dup . cr f:include +"sieve-tests.8th" dup . cr f:include +bye