From 41f898572ea3fc93ca57e670904f13a288eb033f Mon Sep 17 00:00:00 2001 From: Eric Willigers Date: Wed, 10 Jun 2026 10:49:35 +1000 Subject: [PATCH] grep split-second-stopwatch --- concepts/files/.meta/config.json | 6 + concepts/files/about.md | 83 +++++ concepts/files/introduction.md | 47 +++ concepts/files/links.json | 14 + config.json | 48 +++ .../.docs/introduction.md | 13 + .../joiners-journey/.docs/introduction.md | 17 + .../vltava-weather-watch/.docs/hints.md | 36 +++ .../.docs/instructions.md | 59 ++++ .../.docs/introduction.md | 77 +++++ .../vltava-weather-watch/.meta/config.json | 18 ++ .../vltava-weather-watch/.meta/design.md | 62 ++++ .../.meta/exemplar.factor | 17 + .../exercism-tools/exercism-tools.factor | 34 ++ .../vltava-weather-watch-tests.factor | 45 +++ .../vltava-weather-watch.factor | 22 ++ .../grep/.docs/instructions.append.md | 30 ++ exercises/practice/grep/.docs/instructions.md | 27 ++ exercises/practice/grep/.docs/introduction.md | 5 + exercises/practice/grep/.meta/config.json | 24 ++ exercises/practice/grep/.meta/example.factor | 36 +++ exercises/practice/grep/.meta/generator.jl | 19 ++ exercises/practice/grep/.meta/tests.toml | 85 +++++ .../grep/exercism-tools/exercism-tools.factor | 34 ++ .../practice/grep/grep/grep-tests.factor | 129 ++++++++ exercises/practice/grep/grep/grep.factor | 13 + exercises/practice/grep/iliad.txt | 9 + exercises/practice/grep/midsummer-night.txt | 7 + exercises/practice/grep/paradise-lost.txt | 8 + .../.docs/instructions.append.md | 25 ++ .../.docs/instructions.md | 22 ++ .../.docs/introduction.md | 6 + .../split-second-stopwatch/.meta/config.json | 19 ++ .../.meta/example.factor | 55 ++++ .../split-second-stopwatch/.meta/generator.jl | 70 +++++ .../split-second-stopwatch/.meta/tests.toml | 97 ++++++ .../exercism-tools/exercism-tools.factor | 34 ++ .../split-second-stopwatch-tests.factor | 293 ++++++++++++++++++ .../split-second-stopwatch.factor | 45 +++ 39 files changed, 1690 insertions(+) create mode 100644 concepts/files/.meta/config.json create mode 100644 concepts/files/about.md create mode 100644 concepts/files/introduction.md create mode 100644 concepts/files/links.json create mode 100644 exercises/concept/vltava-weather-watch/.docs/hints.md create mode 100644 exercises/concept/vltava-weather-watch/.docs/instructions.md create mode 100644 exercises/concept/vltava-weather-watch/.docs/introduction.md create mode 100644 exercises/concept/vltava-weather-watch/.meta/config.json create mode 100644 exercises/concept/vltava-weather-watch/.meta/design.md create mode 100644 exercises/concept/vltava-weather-watch/.meta/exemplar.factor create mode 100644 exercises/concept/vltava-weather-watch/exercism-tools/exercism-tools.factor create mode 100644 exercises/concept/vltava-weather-watch/vltava-weather-watch/vltava-weather-watch-tests.factor create mode 100644 exercises/concept/vltava-weather-watch/vltava-weather-watch/vltava-weather-watch.factor create mode 100644 exercises/practice/grep/.docs/instructions.append.md create mode 100644 exercises/practice/grep/.docs/instructions.md create mode 100644 exercises/practice/grep/.docs/introduction.md create mode 100644 exercises/practice/grep/.meta/config.json create mode 100644 exercises/practice/grep/.meta/example.factor create mode 100644 exercises/practice/grep/.meta/generator.jl create mode 100644 exercises/practice/grep/.meta/tests.toml create mode 100644 exercises/practice/grep/exercism-tools/exercism-tools.factor create mode 100644 exercises/practice/grep/grep/grep-tests.factor create mode 100644 exercises/practice/grep/grep/grep.factor create mode 100644 exercises/practice/grep/iliad.txt create mode 100644 exercises/practice/grep/midsummer-night.txt create mode 100644 exercises/practice/grep/paradise-lost.txt create mode 100644 exercises/practice/split-second-stopwatch/.docs/instructions.append.md create mode 100644 exercises/practice/split-second-stopwatch/.docs/instructions.md create mode 100644 exercises/practice/split-second-stopwatch/.docs/introduction.md create mode 100644 exercises/practice/split-second-stopwatch/.meta/config.json create mode 100644 exercises/practice/split-second-stopwatch/.meta/example.factor create mode 100644 exercises/practice/split-second-stopwatch/.meta/generator.jl create mode 100644 exercises/practice/split-second-stopwatch/.meta/tests.toml create mode 100644 exercises/practice/split-second-stopwatch/exercism-tools/exercism-tools.factor create mode 100644 exercises/practice/split-second-stopwatch/split-second-stopwatch/split-second-stopwatch-tests.factor create mode 100644 exercises/practice/split-second-stopwatch/split-second-stopwatch/split-second-stopwatch.factor diff --git a/concepts/files/.meta/config.json b/concepts/files/.meta/config.json new file mode 100644 index 00000000..47c10d97 --- /dev/null +++ b/concepts/files/.meta/config.json @@ -0,0 +1,6 @@ +{ + "authors": [ + "keiravillekode" + ], + "blurb": "Read, write, and append to files on disk with the whole-file and scoped words of Factor's io.files vocabulary." +} diff --git a/concepts/files/about.md b/concepts/files/about.md new file mode 100644 index 00000000..22fa6e64 --- /dev/null +++ b/concepts/files/about.md @@ -0,0 +1,83 @@ +# About + +A file is a stream with a name on disk. The [`io.files`][io.files] vocabulary +offers two ways to work with one: whole-file convenience words that read or +write everything in a single call, and stream openers (the `with-…` +combinators) for reading or writing incrementally. Both rest on the same +[stream protocol][stream-protocol] as the in-memory streams from the +`io-streams` concept — a `` and a `` answer the same +words. + +Every file word takes an **encoding**, the scheme that maps bytes to characters +and back. For text this is almost always [`utf8`][utf8] from +`io.encodings.utf8`; binary work uses `latin1` or a `` encoding. + +## Whole-file reading + +``` +file-contents ( path encoding -- str ) +file-lines ( path encoding -- seq ) +``` + +`file-contents` returns the entire file as one string. `file-lines` returns its +lines as an array, with the line terminators stripped off — handy when each +line is a record. + +```factor +USING: io.encodings.utf8 io.files ; + +"forecast.txt" utf8 file-contents ! => "sunny\ncloudy\n" +"forecast.txt" utf8 file-lines ! => { "sunny" "cloudy" } +``` + +## Whole-file writing + +``` +set-file-contents ( str path encoding -- ) +set-file-lines ( seq path encoding -- ) +``` + +Both **replace** the file, creating it if it does not exist. +`set-file-contents` writes a string verbatim; `set-file-lines` writes each +element of a sequence on its own line, supplying the newlines. + +```factor +{ "sunny" "cloudy" } "forecast.txt" utf8 set-file-lines +! forecast.txt now holds "sunny\ncloudy\n" +``` + +## Scoped streams + +When you want to add to a file, or read and write piece by piece, open it with a +`with-…` combinator and use the ambient stream words inside the quotation: + +``` +with-file-reader ( path encoding quot -- ) +with-file-writer ( path encoding quot -- ) +with-file-appender ( path encoding quot -- ) +``` + +```factor +USING: io io.encodings.utf8 io.files ; + +"log.txt" utf8 [ "another line" print ] with-file-appender +``` + +`with-file-writer` truncates the file first; `with-file-appender` keeps the +existing contents and writes after them. Each one is a destructor scope: the +file handle is closed when the quotation finishes, even if it throws — the same +guarantee the `boatswains-bilge` combinators provide, here specialised to +files. + +## Whole-file or scoped? + +Reach for `file-contents` / `file-lines` / `set-file-contents` / +`set-file-lines` when the file is small enough to hold in memory and you want +the whole thing at once. Reach for the `with-…` openers when you need to append, +to stream a large file a line at a time with `readln`, or to interleave reads +and writes — anywhere holding the entire file in memory would be wasteful or +wrong. + +[io.files]: https://docs.factorcode.org/content/vocab-io.files.html +[utf8]: https://docs.factorcode.org/content/vocab-io.encodings.utf8.html +[stream-protocol]: https://docs.factorcode.org/content/article-stream-protocol.html diff --git a/concepts/files/introduction.md b/concepts/files/introduction.md new file mode 100644 index 00000000..8fee38f0 --- /dev/null +++ b/concepts/files/introduction.md @@ -0,0 +1,47 @@ +# Introduction + +A file is a stream with a name on disk. The [`io.files`][io.files] vocabulary +reads and writes files either whole — in a single call — or incrementally +through a scoped stream. Every file word takes an **encoding**; for text that +is almost always [`utf8`][utf8] from `io.encodings.utf8`. + +## Reading + +``` +file-contents ( path encoding -- str ) +file-lines ( path encoding -- seq ) +``` + +`file-contents` returns the whole file as one string. `file-lines` returns its +lines as an array, with the line breaks removed. + +## Writing + +``` +set-file-contents ( str path encoding -- ) +set-file-lines ( seq path encoding -- ) +``` + +Both replace the file (creating it if needed). `set-file-lines` writes one +element per line, adding the newlines for you. + +## Appending and incremental I/O + +The `with-…` combinators open a file as the ambient stream for a quotation and +close it afterward — a destructor scope, like the stream combinators in +`channel-chatter`. + +``` +with-file-reader ( path encoding quot -- ) +with-file-writer ( path encoding quot -- ) +with-file-appender ( path encoding quot -- ) +``` + +```factor +USING: io io.encodings.utf8 io.files ; + +"log.txt" utf8 [ "another line" print ] with-file-appender +``` + +[io.files]: https://docs.factorcode.org/content/vocab-io.files.html +[utf8]: https://docs.factorcode.org/content/vocab-io.encodings.utf8.html diff --git a/concepts/files/links.json b/concepts/files/links.json new file mode 100644 index 00000000..355c9a1b --- /dev/null +++ b/concepts/files/links.json @@ -0,0 +1,14 @@ +[ + { + "url": "https://docs.factorcode.org/content/vocab-io.files.html", + "description": "io.files — reading, writing, and appending to files" + }, + { + "url": "https://docs.factorcode.org/content/vocab-io.encodings.utf8.html", + "description": "io.encodings.utf8 — the utf8 encoding for text files" + }, + { + "url": "https://docs.factorcode.org/content/article-stream-protocol.html", + "description": "The stream protocol that file streams implement" + } +] diff --git a/config.json b/config.json index b15fe6ea..dd84d371 100644 --- a/config.json +++ b/config.json @@ -634,6 +634,20 @@ "locals" ], "status": "beta" + }, + { + "slug": "vltava-weather-watch", + "name": "Vltava Weather Watch", + "uuid": "edbe1108-0c38-47a1-9917-d2d6f9df1752", + "concepts": [ + "files" + ], + "prerequisites": [ + "io-streams", + "sequences", + "strings" + ], + "status": "beta" } ], "practice": [ @@ -1796,6 +1810,21 @@ ], "difficulty": 5 }, + { + "slug": "split-second-stopwatch", + "name": "Split-Second Stopwatch", + "uuid": "b4f33c97-64e9-41c1-804f-fa2453d3f1da", + "practices": [ + "tuples" + ], + "prerequisites": [ + "tuples", + "errors", + "sequences", + "strings" + ], + "difficulty": 5 + }, { "slug": "square-root", "name": "Square Root", @@ -2107,6 +2136,20 @@ ], "difficulty": 7 }, + { + "slug": "grep", + "name": "Grep", + "uuid": "bb9023f5-0baf-4e6a-a219-99390fd0aebd", + "practices": [ + "files" + ], + "prerequisites": [ + "files", + "sequences", + "strings" + ], + "difficulty": 7 + }, { "slug": "paasio", "name": "PaaS I/O", @@ -2511,6 +2554,11 @@ "uuid": "6510ec99-2e86-4a29-a57a-a733f13a1365", "slug": "combinatorics", "name": "Combinatorics" + }, + { + "uuid": "30538350-2959-4c56-b76b-2aedd7c919a0", + "slug": "files", + "name": "Files" } ], "key_features": [ diff --git a/exercises/concept/backyard-birdwatcher/.docs/introduction.md b/exercises/concept/backyard-birdwatcher/.docs/introduction.md index ef9fa75d..92c396e2 100644 --- a/exercises/concept/backyard-birdwatcher/.docs/introduction.md +++ b/exercises/concept/backyard-birdwatcher/.docs/introduction.md @@ -95,6 +95,19 @@ When you have a *start and a length*, add them to get the end: (That's the index range `[2, 5)` — three characters starting at position 2.) +The reverse question — *where* a subsequence occurs — is answered +by `subseq-index`, which returns the starting index of the first +match, or `f` when there is none: + +``` +subseq-index ( seq subseq -- i/f ) +``` + +```factor +"hello world" "o w" subseq-index . ! => 4 +"hello world" "xyz" subseq-index . ! => f +``` + ## Padding ``` diff --git a/exercises/concept/joiners-journey/.docs/introduction.md b/exercises/concept/joiners-journey/.docs/introduction.md index 7f7239fd..dfd5a307 100644 --- a/exercises/concept/joiners-journey/.docs/introduction.md +++ b/exercises/concept/joiners-journey/.docs/introduction.md @@ -170,5 +170,22 @@ bi* ( x y q1 q2 -- r1 r2 ) Reach for `bi@` when both values get the same treatment, and for `bi*` when each needs its own. +## `tri*` — different operations on three values + +`tri*` (in [`combinators`][combinators]) extends `bi*` to three +values and three quotations, applying each quotation to one value +in order: + +``` +tri* ( x y z q1 q2 q3 -- r1 r2 r3 ) +``` + +```factor +1 2 3 [ sq ] [ neg ] [ 10 * ] tri* .s +! => 1 +! => -2 +! => 30 +``` + [kernel]: https://docs.factorcode.org/content/vocab-kernel.html [combinators]: https://docs.factorcode.org/content/vocab-combinators.html diff --git a/exercises/concept/vltava-weather-watch/.docs/hints.md b/exercises/concept/vltava-weather-watch/.docs/hints.md new file mode 100644 index 00000000..b1c7d9f3 --- /dev/null +++ b/exercises/concept/vltava-weather-watch/.docs/hints.md @@ -0,0 +1,36 @@ +# Hints + +## General + +- Every reading and writing word lives in [`io.files`][io.files]; the `utf8` + encoding they all take comes from [`io.encodings.utf8`][utf8]. + +## 1. Read every reading + +- `file-lines` returns an array of lines with the newlines removed. + +## 2. Find the latest reading + +- Read the lines with `file-lines`, then take the `last` one (from + `sequences`). + +## 3. Read the raw log + +- `file-contents` returns the file as a single string, newlines and all. + +## 4. Record a reading + +- Open the file for appending and `print` the reading inside the quotation: + `with-file-appender ( path encoding quot -- )`. The reading is used inside + the quotation while the path is consumed before it, so `::` locals keep the + body readable: + `:: record-reading ( reading path -- ) path utf8 [ reading print ] + with-file-appender ;`. + +## 5. Rewrite the log + +- `set-file-lines` writes each element of a sequence on its own line, adding + the newlines for you. + +[io.files]: https://docs.factorcode.org/content/vocab-io.files.html +[utf8]: https://docs.factorcode.org/content/vocab-io.encodings.utf8.html diff --git a/exercises/concept/vltava-weather-watch/.docs/instructions.md b/exercises/concept/vltava-weather-watch/.docs/instructions.md new file mode 100644 index 00000000..11dd43d2 --- /dev/null +++ b/exercises/concept/vltava-weather-watch/.docs/instructions.md @@ -0,0 +1,59 @@ +# Instructions + +You run a little weather post on the bank of the Vltava in Prague, and every +shift you jot the temperature readings into a plain-text log — one reading per +line. Five small words handle a shift's worth of bookkeeping. + +Each word is handed the path to the log file. + +## 1. Read every reading + +Define `read-readings` to return all of the log's readings as an array of +strings, one per line. + +```factor +"weather.log" read-readings . +! => { "21.5" "19.0" "22.3" } +``` + +## 2. Find the latest reading + +Define `latest-reading` to return the most recent reading — the last line of +the log. + +```factor +"weather.log" latest-reading . +! => "22.3" +``` + +## 3. Read the raw log + +Define `log-text` to return the whole log file as a single string, exactly as +stored. + +```factor +"weather.log" log-text . +! => "21.5\n19.0\n22.3\n" +``` + +## 4. Record a reading + +Define `record-reading` to append a new reading to the end of the log as its +own line, leaving the earlier readings untouched. Returns nothing. + +```factor +"23.1" "weather.log" record-reading +"weather.log" log-text . +! => "21.5\n19.0\n22.3\n23.1\n" +``` + +## 5. Rewrite the log + +Define `rewrite-log` to replace the whole log with a fresh array of readings, +one per line. + +```factor +{ "10.0" "11.0" } "weather.log" rewrite-log +"weather.log" log-text . +! => "10.0\n11.0\n" +``` diff --git a/exercises/concept/vltava-weather-watch/.docs/introduction.md b/exercises/concept/vltava-weather-watch/.docs/introduction.md new file mode 100644 index 00000000..c0d03766 --- /dev/null +++ b/exercises/concept/vltava-weather-watch/.docs/introduction.md @@ -0,0 +1,77 @@ +# Introduction + +A file is just a stream with a name on disk. The [`io.files`][io.files] +vocabulary gives you two ways to work with one: whole-file convenience words +that read or write everything in a single call, and stream openers for when you +want to read or write incrementally. + +Every word that touches a file takes an **encoding** — the scheme that turns +bytes into characters and back. For text, that is almost always +[`utf8`][utf8] from `io.encodings.utf8`. + +## Reading a whole file + +``` +file-contents ( path encoding -- str ) +file-lines ( path encoding -- seq ) +``` + +`file-contents` returns the entire file as one string; `file-lines` returns an +array of its lines, with the line breaks stripped off. + +```factor +USING: io.encodings.utf8 io.files prettyprint ; + +"sunny\ncloudy\n" "forecast.txt" utf8 set-file-contents + +"forecast.txt" utf8 file-contents . +! => "sunny\ncloudy\n" + +"forecast.txt" utf8 file-lines . +! => { "sunny" "cloudy" } +``` + +## Writing a whole file + +``` +set-file-contents ( str path encoding -- ) +set-file-lines ( seq path encoding -- ) +``` + +Both **replace** the file's contents (creating it if needed). +`set-file-contents` writes a string verbatim; `set-file-lines` writes each +element of a sequence on its own line, adding the newlines for you. + +```factor +USING: io.encodings.utf8 io.files prettyprint ; + +{ "sunny" "cloudy" } "forecast.txt" utf8 set-file-lines +"forecast.txt" utf8 file-contents . +! => "sunny\ncloudy\n" +``` + +## Appending to a file + +To add to a file without erasing what is already there, open it with a +`with-…` combinator and write to the ambient stream inside the quotation: + +``` +with-file-appender ( path encoding quot -- ) +with-file-reader ( path encoding quot -- ) +with-file-writer ( path encoding quot -- ) +``` + +```factor +USING: io io.encodings.utf8 io.files prettyprint ; + +"sunny\ncloudy\n" "forecast.txt" utf8 set-file-contents +"forecast.txt" utf8 [ "rainy" print ] with-file-appender +"forecast.txt" utf8 file-contents . +! => "sunny\ncloudy\nrainy\n" +``` + +Like every `with-…` combinator, these are destructor scopes: the file handle is +closed when the quotation finishes, even if it throws. + +[io.files]: https://docs.factorcode.org/content/vocab-io.files.html +[utf8]: https://docs.factorcode.org/content/vocab-io.encodings.utf8.html diff --git a/exercises/concept/vltava-weather-watch/.meta/config.json b/exercises/concept/vltava-weather-watch/.meta/config.json new file mode 100644 index 00000000..e29f9420 --- /dev/null +++ b/exercises/concept/vltava-weather-watch/.meta/config.json @@ -0,0 +1,18 @@ +{ + "authors": [ + "keiravillekode" + ], + "files": { + "solution": [ + "vltava-weather-watch/vltava-weather-watch.factor" + ], + "test": [ + "vltava-weather-watch/vltava-weather-watch-tests.factor" + ], + "exemplar": [ + ".meta/exemplar.factor" + ] + }, + "icon": "the-weather-in-deather", + "blurb": "Read, write, and append to files on disk with the whole-file and scoped words of the io.files vocabulary." +} diff --git a/exercises/concept/vltava-weather-watch/.meta/design.md b/exercises/concept/vltava-weather-watch/.meta/design.md new file mode 100644 index 00000000..b94a477b --- /dev/null +++ b/exercises/concept/vltava-weather-watch/.meta/design.md @@ -0,0 +1,62 @@ +# Design + +## Goal + +Teach reading from and writing to real files on disk with `io.files`, building +directly on the stream story from `channel-chatter`. The student has already +read from and written to in-memory string streams; here those same operations +land on named files via the whole-file convenience words and a `with-…` +appender. + +## Learning objectives + +- Read a whole file with `file-contents` and as lines with `file-lines`. +- Replace a file's contents with `set-file-contents` / `set-file-lines`. +- Append to a file with `with-file-appender`, writing to the ambient stream. +- Pass the `utf8` encoding (from `io.encodings.utf8`) to every file word. +- Recognise `with-file-reader` / `with-file-writer` / `with-file-appender` as + the same destructor-scoped `with-…` family seen in `channel-chatter`. + +## Out of scope + +- Implementing the stream protocol — that is `telegraphers-tape`. +- Incremental reading (`stream-read`, `stream-readln`) — covered as a protocol + in the stream exercises; this exercise stays with whole-file words plus an + appender. +- Paths, globbing, directory traversal, and file metadata (`io.pathnames`, + `io.directories`). +- Encodings beyond `utf8`. + +## Concepts + +- `files`: whole-file reading (`file-contents`, `file-lines`), whole-file + writing (`set-file-contents`, `set-file-lines`), and appending with + `with-file-appender`, all parameterised by an encoding. + +## Prerequisites + +- `io-streams` — taught in `channel-chatter`. Files are streams; the `with-…` + appender writes to the ambient output exactly like the string-writer tasks. +- `sequences` — taught in `backyard-birdwatcher`. `file-lines` returns an + array; `last` (task 2) consumes it and `set-file-lines` writes one back. +- `strings` — taught in `log-levels`. Readings and log contents are strings. + +## Tasks ramp + +1. **`read-readings`** — `file-lines`. The first read, line-oriented. +2. **`latest-reading`** — `file-lines last`. Picks a line out of the array, + reusing the sequence skills from the prerequisite. +3. **`log-text`** — `file-contents`. The whole-file string form, contrasted + with the line form. +4. **`record-reading`** — `with-file-appender` with `print`. First write; the + `with-…` scope and ambient output mirror `channel-chatter`'s `broadcast`. +5. **`rewrite-log`** — `set-file-lines`. Replaces the file wholesale, the + inverse of task 1. + +## Why a single log file in tests + +Each test rewrites `weather.log` in the working directory with +`set-file-contents` before exercising a word, and the write tasks read the file +back to confirm the change. Reusing one fixed filename keeps the tests +self-contained and deterministic — no temp-path juggling — while still hitting +the real on-disk code path. diff --git a/exercises/concept/vltava-weather-watch/.meta/exemplar.factor b/exercises/concept/vltava-weather-watch/.meta/exemplar.factor new file mode 100644 index 00000000..1b907e0b --- /dev/null +++ b/exercises/concept/vltava-weather-watch/.meta/exemplar.factor @@ -0,0 +1,17 @@ +USING: io io.encodings.utf8 io.files kernel locals sequences ; +IN: vltava-weather-watch + +: read-readings ( path -- readings ) + utf8 file-lines ; + +: latest-reading ( path -- reading ) + utf8 file-lines last ; + +: log-text ( path -- text ) + utf8 file-contents ; + +:: record-reading ( reading path -- ) + path utf8 [ reading print ] with-file-appender ; + +: rewrite-log ( readings path -- ) + utf8 set-file-lines ; diff --git a/exercises/concept/vltava-weather-watch/exercism-tools/exercism-tools.factor b/exercises/concept/vltava-weather-watch/exercism-tools/exercism-tools.factor new file mode 100644 index 00000000..a21c60cb --- /dev/null +++ b/exercises/concept/vltava-weather-watch/exercism-tools/exercism-tools.factor @@ -0,0 +1,34 @@ +USING: accessors command-line continuations debugger io kernel + lexer namespaces sequences source-files.errors.debugger + system tools.test vocabs vocabs.loader ; +IN: exercism-tools + +SYNTAX: STOP-HERE + lexer get [ text>> length ] keep line<< ; + +SYNTAX: TASK: + lexer get next-line ; + +! Label the test that follows with its description. +: description ( str -- ) + "###DESC### " write print ; + +! Print one failure block in a stable, parser-friendly form. +:: print-failure ( failure -- ) + "###FAIL_BEGIN###" print + failure error-location print + failure error>> [ error. ] [ 2drop ] recover + "###FAIL_END###" print + flush ; + +: print-failures ( -- ) + test-failures get [ print-failure ] each ; + +: run-exercism-tests ( -- ) + vocab-roots [ "." prefix ] change-global + command-line get first + [ require ] [ test ] bi + test-failures get empty? + [ 0 exit ] [ print-failures 1 exit ] if ; + +MAIN: run-exercism-tests diff --git a/exercises/concept/vltava-weather-watch/vltava-weather-watch/vltava-weather-watch-tests.factor b/exercises/concept/vltava-weather-watch/vltava-weather-watch/vltava-weather-watch-tests.factor new file mode 100644 index 00000000..fad34ca8 --- /dev/null +++ b/exercises/concept/vltava-weather-watch/vltava-weather-watch/vltava-weather-watch-tests.factor @@ -0,0 +1,45 @@ +USING: exercism-tools io.encodings.utf8 io.files kernel + vltava-weather-watch tools.test ; +IN: vltava-weather-watch.tests + +TASK: 1 read-readings +"21.5\n19.0\n22.3\n" "weather.log" utf8 set-file-contents +{ { "21.5" "19.0" "22.3" } } +[ "weather.log" read-readings ] unit-test + +STOP-HERE + +"7.0\n" "weather.log" utf8 set-file-contents +{ { "7.0" } } [ "weather.log" read-readings ] unit-test + +TASK: 2 latest-reading +"21.5\n19.0\n22.3\n" "weather.log" utf8 set-file-contents +{ "22.3" } [ "weather.log" latest-reading ] unit-test + +"5.5\n" "weather.log" utf8 set-file-contents +{ "5.5" } [ "weather.log" latest-reading ] unit-test + +TASK: 3 log-text +"21.5\n19.0\n" "weather.log" utf8 set-file-contents +{ "21.5\n19.0\n" } [ "weather.log" log-text ] unit-test + +TASK: 4 record-reading +"21.5\n19.0\n" "weather.log" utf8 set-file-contents +{ "21.5\n19.0\n22.3\n" } [ + "22.3" "weather.log" record-reading + "weather.log" utf8 file-contents +] unit-test + +! appending to an empty log writes the first line +"" "weather.log" utf8 set-file-contents +{ "18.0\n" } [ + "18.0" "weather.log" record-reading + "weather.log" utf8 file-contents +] unit-test + +TASK: 5 rewrite-log +"99.9\n98.8\n" "weather.log" utf8 set-file-contents +{ "21.5\n19.0\n22.3\n" } [ + { "21.5" "19.0" "22.3" } "weather.log" rewrite-log + "weather.log" utf8 file-contents +] unit-test diff --git a/exercises/concept/vltava-weather-watch/vltava-weather-watch/vltava-weather-watch.factor b/exercises/concept/vltava-weather-watch/vltava-weather-watch/vltava-weather-watch.factor new file mode 100644 index 00000000..37e74a6c --- /dev/null +++ b/exercises/concept/vltava-weather-watch/vltava-weather-watch/vltava-weather-watch.factor @@ -0,0 +1,22 @@ +USING: kernel ; +IN: vltava-weather-watch + +! Each word receives a path to the station's log file. Reach for +! `file-lines`, `file-contents`, `with-file-appender`, and +! `set-file-lines` from the `io.files` vocabulary, paired with the +! `utf8` encoding from `io.encodings.utf8`. + +: read-readings ( path -- readings ) + "unimplemented" throw ; + +: latest-reading ( path -- reading ) + "unimplemented" throw ; + +: log-text ( path -- text ) + "unimplemented" throw ; + +: record-reading ( reading path -- ) + "unimplemented" throw ; + +: rewrite-log ( readings path -- ) + "unimplemented" throw ; diff --git a/exercises/practice/grep/.docs/instructions.append.md b/exercises/practice/grep/.docs/instructions.append.md new file mode 100644 index 00000000..26aef03e --- /dev/null +++ b/exercises/practice/grep/.docs/instructions.append.md @@ -0,0 +1,30 @@ +# Instructions append + +## Words + +Implement: + +- `grep ( pattern flags files -- lines )` + +`pattern` is the search string, `flags` is an array of zero or more of the +flag strings `"-n"`, `"-l"`, `"-i"`, `"-v"`, and `"-x"`, and `files` is an +array of one or more file names. Return the matching lines (with any requested +file-name and line-number prefixes) as an array of strings. + +When the `-l` flag is given, return an array of the names of the files that +contain at least one match. + +## Reading files + +Read a file's lines with `file-lines`, passing an encoding: + +```factor +USING: io.encodings.utf8 io.files ; + +"iliad.txt" utf8 file-lines +! => { "Achilles sing, O Goddess! Peleus' son;" ... } +``` + +The three fixture files (`iliad.txt`, `midsummer-night.txt`, and +`paradise-lost.txt`) ship with the exercise in the working directory, so +reading them by name just works. diff --git a/exercises/practice/grep/.docs/instructions.md b/exercises/practice/grep/.docs/instructions.md new file mode 100644 index 00000000..004f28ac --- /dev/null +++ b/exercises/practice/grep/.docs/instructions.md @@ -0,0 +1,27 @@ +# Instructions + +Search files for lines matching a search string and return all matching lines. + +The Unix [`grep`][grep] command searches files for lines that match a regular expression. +Your task is to implement a simplified `grep` command, which supports searching for fixed strings. + +The `grep` command takes three arguments: + +1. The string to search for. +2. Zero or more flags for customizing the command's behavior. +3. One or more files to search in. + +It then reads the contents of the specified files (in the order specified), finds the lines that contain the search string, and finally returns those lines in the order in which they were found. +When searching in multiple files, each matching line is prepended by the file name and a colon (':'). + +## Flags + +The `grep` command supports the following flags: + +- `-n` Prepend the line number and a colon (':') to each line in the output, placing the number after the filename (if present). +- `-l` Output only the names of the files that contain at least one matching line. +- `-i` Match using a case-insensitive comparison. +- `-v` Invert the program -- collect all lines that fail to match. +- `-x` Search only for lines where the search string matches the entire line. + +[grep]: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/grep.html diff --git a/exercises/practice/grep/.docs/introduction.md b/exercises/practice/grep/.docs/introduction.md new file mode 100644 index 00000000..40412904 --- /dev/null +++ b/exercises/practice/grep/.docs/introduction.md @@ -0,0 +1,5 @@ +# Introduction + +You have taken a job at a local library helping organize their collection of old books. +The student patrons are often hunting for half-remembered quotes to cite in their term papers. +Rather than manually read every book from cover to cover, you decide to build a small tool to scan them, looking for these partial quotes. diff --git a/exercises/practice/grep/.meta/config.json b/exercises/practice/grep/.meta/config.json new file mode 100644 index 00000000..e5dcf5d0 --- /dev/null +++ b/exercises/practice/grep/.meta/config.json @@ -0,0 +1,24 @@ +{ + "authors": [ + "keiravillekode" + ], + "files": { + "solution": [ + "grep/grep.factor" + ], + "test": [ + "grep/grep-tests.factor" + ], + "example": [ + ".meta/example.factor" + ], + "editor": [ + "iliad.txt", + "midsummer-night.txt", + "paradise-lost.txt" + ] + }, + "blurb": "Search a file for lines matching a regular expression pattern.", + "source": "Conversation with Nate Foster.", + "source_url": "https://www.cs.cornell.edu/courses/cs3110/2014sp/hw/0/ps0.pdf" +} diff --git a/exercises/practice/grep/.meta/example.factor b/exercises/practice/grep/.meta/example.factor new file mode 100644 index 00000000..3e60461f --- /dev/null +++ b/exercises/practice/grep/.meta/example.factor @@ -0,0 +1,36 @@ +USING: accessors io.encodings.utf8 io.files kernel locals math + math.parser sequences unicode ; +IN: grep + +:: line-matches? ( line pattern i? v? x? -- ? ) + i? [ line >lower ] [ line ] if :> text + i? [ pattern >lower ] [ pattern ] if :> pat + x? [ text pat = ] [ text pat subseq-index ] if + v? [ not ] when ; + +: prefix-field ( str field -- str' ) swap ":" glue ; + +:: format-line ( line lineno n? multi? file -- str ) + line + n? [ lineno number>string prefix-field ] when + multi? [ file prefix-field ] when ; + +:: grep ( pattern flags files -- lines ) + "-n" flags member? :> n? + "-l" flags member? :> l? + "-i" flags member? :> i? + "-v" flags member? :> v? + "-x" flags member? :> x? + files length 1 > :> multi? + files [| file | + file utf8 file-lines :> all-lines + l? [ + all-lines [ pattern i? v? x? line-matches? ] any? + [ { file } ] [ { } ] if + ] [ + all-lines [| line idx | + line pattern i? v? x? line-matches? + [ line idx 1 + n? multi? file format-line ] [ f ] if + ] map-index sift + ] if + ] map concat ; diff --git a/exercises/practice/grep/.meta/generator.jl b/exercises/practice/grep/.meta/generator.jl new file mode 100644 index 00000000..8dcf22a7 --- /dev/null +++ b/exercises/practice/grep/.meta/generator.jl @@ -0,0 +1,19 @@ +module Grep + +# The three fixture files (iliad.txt, midsummer-night.txt, paradise-lost.txt) +# ship with the exercise and are present in the working directory when the +# tests run, so the test suite reads them by name — no setup required. + +string_array(arr) = isempty(arr) ? "{ }" : format_string_array(arr) + +function gen_test_case(case) + inp = case["input"] + pattern = "\"$(escape_factor(inp["pattern"]))\"" + flags = string_array(inp["flags"]) + files = string_array(inp["files"]) + # `grep` returns one array, so the expected datastack is that single value. + expected = string_array(case["expected"]) + "{ $(expected) }\n[ $(pattern) $(flags) $(files) grep ]\nunit-test" +end + +end diff --git a/exercises/practice/grep/.meta/tests.toml b/exercises/practice/grep/.meta/tests.toml new file mode 100644 index 00000000..04c51e71 --- /dev/null +++ b/exercises/practice/grep/.meta/tests.toml @@ -0,0 +1,85 @@ +# 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. + +[9049fdfd-53a7-4480-a390-375203837d09] +description = "Test grepping a single file -> One file, one match, no flags" + +[76519cce-98e3-46cd-b287-aac31b1d77d6] +description = "Test grepping a single file -> One file, one match, print line numbers flag" + +[af0b6d3c-e0e8-475e-a112-c0fc10a1eb30] +description = "Test grepping a single file -> One file, one match, case-insensitive flag" + +[ff7af839-d1b8-4856-a53e-99283579b672] +description = "Test grepping a single file -> One file, one match, print file names flag" + +[8625238a-720c-4a16-81f2-924ec8e222cb] +description = "Test grepping a single file -> One file, one match, match entire lines flag" + +[2a6266b3-a60f-475c-a5f5-f5008a717d3e] +description = "Test grepping a single file -> One file, one match, multiple flags" + +[842222da-32e8-4646-89df-0d38220f77a1] +description = "Test grepping a single file -> One file, several matches, no flags" + +[4d84f45f-a1d8-4c2e-a00e-0b292233828c] +description = "Test grepping a single file -> One file, several matches, print line numbers flag" + +[0a483b66-315b-45f5-bc85-3ce353a22539] +description = "Test grepping a single file -> One file, several matches, match entire lines flag" + +[3d2ca86a-edd7-494c-8938-8eeed1c61cfa] +description = "Test grepping a single file -> One file, several matches, case-insensitive flag" + +[1f52001f-f224-4521-9456-11120cad4432] +description = "Test grepping a single file -> One file, several matches, inverted flag" + +[7a6ede7f-7dd5-4364-8bf8-0697c53a09fe] +description = "Test grepping a single file -> One file, no matches, various flags" + +[3d3dfc23-8f2a-4e34-abd6-7b7d140291dc] +description = "Test grepping a single file -> One file, one match, file flag takes precedence over line flag" + +[87b21b24-b788-4d6e-a68b-7afe9ca141fe] +description = "Test grepping a single file -> One file, several matches, inverted and match entire lines flags" + +[ba496a23-6149-41c6-a027-28064ed533e5] +description = "Test grepping multiples files at once -> Multiple files, one match, no flags" + +[4539bd36-6daa-4bc3-8e45-051f69f5aa95] +description = "Test grepping multiples files at once -> Multiple files, several matches, no flags" + +[9fb4cc67-78e2-4761-8e6b-a4b57aba1938] +description = "Test grepping multiples files at once -> Multiple files, several matches, print line numbers flag" + +[aeee1ef3-93c7-4cd5-af10-876f8c9ccc73] +description = "Test grepping multiples files at once -> Multiple files, one match, print file names flag" + +[d69f3606-7d15-4ddf-89ae-01df198e6b6c] +description = "Test grepping multiples files at once -> Multiple files, several matches, case-insensitive flag" + +[82ef739d-6701-4086-b911-007d1a3deb21] +description = "Test grepping multiples files at once -> Multiple files, several matches, inverted flag" + +[77b2eb07-2921-4ea0-8971-7636b44f5d29] +description = "Test grepping multiples files at once -> Multiple files, one match, match entire lines flag" + +[e53a2842-55bb-4078-9bb5-04ac38929989] +description = "Test grepping multiples files at once -> Multiple files, one match, multiple flags" + +[9c4f7f9a-a555-4e32-bb06-4b8f8869b2cb] +description = "Test grepping multiples files at once -> Multiple files, no matches, various flags" + +[ba5a540d-bffd-481b-bd0c-d9a30f225e01] +description = "Test grepping multiples files at once -> Multiple files, several matches, file flag takes precedence over line number flag" + +[ff406330-2f0b-4b17-9ee4-4b71c31dd6d2] +description = "Test grepping multiples files at once -> Multiple files, several matches, inverted and match entire lines flags" diff --git a/exercises/practice/grep/exercism-tools/exercism-tools.factor b/exercises/practice/grep/exercism-tools/exercism-tools.factor new file mode 100644 index 00000000..a21c60cb --- /dev/null +++ b/exercises/practice/grep/exercism-tools/exercism-tools.factor @@ -0,0 +1,34 @@ +USING: accessors command-line continuations debugger io kernel + lexer namespaces sequences source-files.errors.debugger + system tools.test vocabs vocabs.loader ; +IN: exercism-tools + +SYNTAX: STOP-HERE + lexer get [ text>> length ] keep line<< ; + +SYNTAX: TASK: + lexer get next-line ; + +! Label the test that follows with its description. +: description ( str -- ) + "###DESC### " write print ; + +! Print one failure block in a stable, parser-friendly form. +:: print-failure ( failure -- ) + "###FAIL_BEGIN###" print + failure error-location print + failure error>> [ error. ] [ 2drop ] recover + "###FAIL_END###" print + flush ; + +: print-failures ( -- ) + test-failures get [ print-failure ] each ; + +: run-exercism-tests ( -- ) + vocab-roots [ "." prefix ] change-global + command-line get first + [ require ] [ test ] bi + test-failures get empty? + [ 0 exit ] [ print-failures 1 exit ] if ; + +MAIN: run-exercism-tests diff --git a/exercises/practice/grep/grep/grep-tests.factor b/exercises/practice/grep/grep/grep-tests.factor new file mode 100644 index 00000000..1b59b7a3 --- /dev/null +++ b/exercises/practice/grep/grep/grep-tests.factor @@ -0,0 +1,129 @@ +USING: exercism-tools grep io kernel tools.test unicode ; +IN: grep.tests + +"One file, one match, no flags" description +{ { "Of Atreus, Agamemnon, King of men." } } +[ "Agamemnon" { } { "iliad.txt" } grep ] +unit-test + +STOP-HERE + +"One file, one match, print line numbers flag" description +{ { "2:Of that Forbidden Tree, whose mortal tast" } } +[ "Forbidden" { "-n" } { "paradise-lost.txt" } grep ] +unit-test + +"One file, one match, case-insensitive flag" description +{ { "Of that Forbidden Tree, whose mortal tast" } } +[ "FORBIDDEN" { "-i" } { "paradise-lost.txt" } grep ] +unit-test + +"One file, one match, print file names flag" description +{ { "paradise-lost.txt" } } +[ "Forbidden" { "-l" } { "paradise-lost.txt" } grep ] +unit-test + +"One file, one match, match entire lines flag" description +{ { "With loss of Eden, till one greater Man" } } +[ "With loss of Eden, till one greater Man" { "-x" } { "paradise-lost.txt" } grep ] +unit-test + +"One file, one match, multiple flags" description +{ { "9:Of Atreus, Agamemnon, King of men." } } +[ "OF ATREUS, Agamemnon, KIng of MEN." { "-n" "-i" "-x" } { "iliad.txt" } grep ] +unit-test + +"One file, several matches, no flags" description +{ { "Nor how it may concern my modesty," "But I beseech your grace that I may know" "The worst that may befall me in this case," } } +[ "may" { } { "midsummer-night.txt" } grep ] +unit-test + +"One file, several matches, print line numbers flag" description +{ { "3:Nor how it may concern my modesty," "5:But I beseech your grace that I may know" "6:The worst that may befall me in this case," } } +[ "may" { "-n" } { "midsummer-night.txt" } grep ] +unit-test + +"One file, several matches, match entire lines flag" description +{ { } } +[ "may" { "-x" } { "midsummer-night.txt" } grep ] +unit-test + +"One file, several matches, case-insensitive flag" description +{ { "Achilles sing, O Goddess! Peleus' son;" "The noble Chief Achilles from the son" } } +[ "ACHILLES" { "-i" } { "iliad.txt" } grep ] +unit-test + +"One file, several matches, inverted flag" description +{ { "Brought Death into the World, and all our woe," "With loss of Eden, till one greater Man" "Restore us, and regain the blissful Seat," "Sing Heav'nly Muse, that on the secret top" "That Shepherd, who first taught the chosen Seed" } } +[ "Of" { "-v" } { "paradise-lost.txt" } grep ] +unit-test + +"One file, no matches, various flags" description +{ { } } +[ "Gandalf" { "-n" "-l" "-x" "-i" } { "iliad.txt" } grep ] +unit-test + +"One file, one match, file flag takes precedence over line flag" description +{ { "iliad.txt" } } +[ "ten" { "-n" "-l" } { "iliad.txt" } grep ] +unit-test + +"One file, several matches, inverted and match entire lines flags" description +{ { "Achilles sing, O Goddess! Peleus' son;" "His wrath pernicious, who ten thousand woes" "Caused to Achaia's host, sent many a soul" "And Heroes gave (so stood the will of Jove)" "To dogs and to all ravening fowls a prey," "When fierce dispute had separated once" "The noble Chief Achilles from the son" "Of Atreus, Agamemnon, King of men." } } +[ "Illustrious into Ades premature," { "-x" "-v" } { "iliad.txt" } grep ] +unit-test + +"Multiple files, one match, no flags" description +{ { "iliad.txt:Of Atreus, Agamemnon, King of men." } } +[ "Agamemnon" { } { "iliad.txt" "midsummer-night.txt" "paradise-lost.txt" } grep ] +unit-test + +"Multiple files, several matches, no flags" description +{ { "midsummer-night.txt:Nor how it may concern my modesty," "midsummer-night.txt:But I beseech your grace that I may know" "midsummer-night.txt:The worst that may befall me in this case," } } +[ "may" { } { "iliad.txt" "midsummer-night.txt" "paradise-lost.txt" } grep ] +unit-test + +"Multiple files, several matches, print line numbers flag" description +{ { "midsummer-night.txt:5:But I beseech your grace that I may know" "midsummer-night.txt:6:The worst that may befall me in this case," "paradise-lost.txt:2:Of that Forbidden Tree, whose mortal tast" "paradise-lost.txt:6:Sing Heav'nly Muse, that on the secret top" } } +[ "that" { "-n" } { "iliad.txt" "midsummer-night.txt" "paradise-lost.txt" } grep ] +unit-test + +"Multiple files, one match, print file names flag" description +{ { "iliad.txt" "paradise-lost.txt" } } +[ "who" { "-l" } { "iliad.txt" "midsummer-night.txt" "paradise-lost.txt" } grep ] +unit-test + +"Multiple files, several matches, case-insensitive flag" description +{ { "iliad.txt:Caused to Achaia's host, sent many a soul" "iliad.txt:Illustrious into Ades premature," "iliad.txt:And Heroes gave (so stood the will of Jove)" "iliad.txt:To dogs and to all ravening fowls a prey," "midsummer-night.txt:I do entreat your grace to pardon me." "midsummer-night.txt:In such a presence here to plead my thoughts;" "midsummer-night.txt:If I refuse to wed Demetrius." "paradise-lost.txt:Brought Death into the World, and all our woe," "paradise-lost.txt:Restore us, and regain the blissful Seat," "paradise-lost.txt:Sing Heav'nly Muse, that on the secret top" } } +[ "TO" { "-i" } { "iliad.txt" "midsummer-night.txt" "paradise-lost.txt" } grep ] +unit-test + +"Multiple files, several matches, inverted flag" description +{ { "iliad.txt:Achilles sing, O Goddess! Peleus' son;" "iliad.txt:The noble Chief Achilles from the son" "midsummer-night.txt:If I refuse to wed Demetrius." } } +[ "a" { "-v" } { "iliad.txt" "midsummer-night.txt" "paradise-lost.txt" } grep ] +unit-test + +"Multiple files, one match, match entire lines flag" description +{ { "midsummer-night.txt:But I beseech your grace that I may know" } } +[ "But I beseech your grace that I may know" { "-x" } { "iliad.txt" "midsummer-night.txt" "paradise-lost.txt" } grep ] +unit-test + +"Multiple files, one match, multiple flags" description +{ { "paradise-lost.txt:4:With loss of Eden, till one greater Man" } } +[ "WITH LOSS OF EDEN, TILL ONE GREATER MAN" { "-n" "-i" "-x" } { "iliad.txt" "midsummer-night.txt" "paradise-lost.txt" } grep ] +unit-test + +"Multiple files, no matches, various flags" description +{ { } } +[ "Frodo" { "-n" "-l" "-x" "-i" } { "iliad.txt" "midsummer-night.txt" "paradise-lost.txt" } grep ] +unit-test + +"Multiple files, several matches, file flag takes precedence over line number flag" description +{ { "iliad.txt" "paradise-lost.txt" } } +[ "who" { "-n" "-l" } { "iliad.txt" "midsummer-night.txt" "paradise-lost.txt" } grep ] +unit-test + +"Multiple files, several matches, inverted and match entire lines flags" description +{ { "iliad.txt:Achilles sing, O Goddess! Peleus' son;" "iliad.txt:His wrath pernicious, who ten thousand woes" "iliad.txt:Caused to Achaia's host, sent many a soul" "iliad.txt:And Heroes gave (so stood the will of Jove)" "iliad.txt:To dogs and to all ravening fowls a prey," "iliad.txt:When fierce dispute had separated once" "iliad.txt:The noble Chief Achilles from the son" "iliad.txt:Of Atreus, Agamemnon, King of men." "midsummer-night.txt:I do entreat your grace to pardon me." "midsummer-night.txt:I know not by what power I am made bold," "midsummer-night.txt:Nor how it may concern my modesty," "midsummer-night.txt:In such a presence here to plead my thoughts;" "midsummer-night.txt:But I beseech your grace that I may know" "midsummer-night.txt:The worst that may befall me in this case," "midsummer-night.txt:If I refuse to wed Demetrius." "paradise-lost.txt:Of Mans First Disobedience, and the Fruit" "paradise-lost.txt:Of that Forbidden Tree, whose mortal tast" "paradise-lost.txt:Brought Death into the World, and all our woe," "paradise-lost.txt:With loss of Eden, till one greater Man" "paradise-lost.txt:Restore us, and regain the blissful Seat," "paradise-lost.txt:Sing Heav'nly Muse, that on the secret top" "paradise-lost.txt:Of Oreb, or of Sinai, didst inspire" "paradise-lost.txt:That Shepherd, who first taught the chosen Seed" } } +[ "Illustrious into Ades premature," { "-x" "-v" } { "iliad.txt" "midsummer-night.txt" "paradise-lost.txt" } grep ] +unit-test diff --git a/exercises/practice/grep/grep/grep.factor b/exercises/practice/grep/grep/grep.factor new file mode 100644 index 00000000..c6728dbb --- /dev/null +++ b/exercises/practice/grep/grep/grep.factor @@ -0,0 +1,13 @@ +USING: kernel ; +IN: grep + +! `grep` receives the search string, an array of flag strings +! (any of "-n", "-l", "-i", "-v", "-x"), and an array of file +! names. Read each file in turn, find the matching lines, and +! return them as an array of strings. +! +! Reading a file's lines: ` utf8 file-lines` (from the +! `io.files` and `io.encodings.utf8` vocabularies). + +: grep ( pattern flags files -- lines ) + "unimplemented" throw ; diff --git a/exercises/practice/grep/iliad.txt b/exercises/practice/grep/iliad.txt new file mode 100644 index 00000000..3286ff9c --- /dev/null +++ b/exercises/practice/grep/iliad.txt @@ -0,0 +1,9 @@ +Achilles sing, O Goddess! Peleus' son; +His wrath pernicious, who ten thousand woes +Caused to Achaia's host, sent many a soul +Illustrious into Ades premature, +And Heroes gave (so stood the will of Jove) +To dogs and to all ravening fowls a prey, +When fierce dispute had separated once +The noble Chief Achilles from the son +Of Atreus, Agamemnon, King of men. diff --git a/exercises/practice/grep/midsummer-night.txt b/exercises/practice/grep/midsummer-night.txt new file mode 100644 index 00000000..f1e3abe1 --- /dev/null +++ b/exercises/practice/grep/midsummer-night.txt @@ -0,0 +1,7 @@ +I do entreat your grace to pardon me. +I know not by what power I am made bold, +Nor how it may concern my modesty, +In such a presence here to plead my thoughts; +But I beseech your grace that I may know +The worst that may befall me in this case, +If I refuse to wed Demetrius. diff --git a/exercises/practice/grep/paradise-lost.txt b/exercises/practice/grep/paradise-lost.txt new file mode 100644 index 00000000..159338e5 --- /dev/null +++ b/exercises/practice/grep/paradise-lost.txt @@ -0,0 +1,8 @@ +Of Mans First Disobedience, and the Fruit +Of that Forbidden Tree, whose mortal tast +Brought Death into the World, and all our woe, +With loss of Eden, till one greater Man +Restore us, and regain the blissful Seat, +Sing Heav'nly Muse, that on the secret top +Of Oreb, or of Sinai, didst inspire +That Shepherd, who first taught the chosen Seed diff --git a/exercises/practice/split-second-stopwatch/.docs/instructions.append.md b/exercises/practice/split-second-stopwatch/.docs/instructions.append.md new file mode 100644 index 00000000..db9bfba0 --- /dev/null +++ b/exercises/practice/split-second-stopwatch/.docs/instructions.append.md @@ -0,0 +1,25 @@ +# Instructions append + +## Words + +Define a `stopwatch` tuple and a constructor ` ( -- stopwatch )` +that returns a new stopwatch in the `"ready"` state with no elapsed time and +no previous laps. + +Provide these words operating on a stopwatch: + +- `state ( stopwatch -- str )` — `"ready"`, `"running"`, or `"stopped"`. +- `current-lap ( stopwatch -- str )` — the current lap's elapsed time as an + `"HH:MM:SS"` string. +- `total ( stopwatch -- str )` — the combined elapsed time of the current lap + and all previous laps, as an `"HH:MM:SS"` string. +- `previous-laps ( stopwatch -- seq )` — an array of the previously recorded + laps, each as an `"HH:MM:SS"` string. +- `start ( stopwatch -- )`, `stop ( stopwatch -- )`, `reset ( stopwatch -- )`, + and `lap ( stopwatch -- )` — the four commands from the table above. Calling + one from a state that does not allow it should `throw` an error. + +Time is advanced with: + +- `advance-time ( str stopwatch -- )` — add the `"HH:MM:SS"` duration to the + current lap. This only has an effect while the stopwatch is running. diff --git a/exercises/practice/split-second-stopwatch/.docs/instructions.md b/exercises/practice/split-second-stopwatch/.docs/instructions.md new file mode 100644 index 00000000..30bdc988 --- /dev/null +++ b/exercises/practice/split-second-stopwatch/.docs/instructions.md @@ -0,0 +1,22 @@ +# Instructions + +Your task is to build a stopwatch to keep precise track of lap times. + +The stopwatch uses four commands (start, stop, lap, and reset) to keep track of: + +1. The current lap's tracked time +2. Previously recorded lap times + +What commands can be used depends on which state the stopwatch is in: + +1. Ready: initial state +2. Running: tracking time +3. Stopped: not tracking time + +| Command | Begin state | End state | Effect | +| ------- | ----------- | --------- | -------------------------------------------------------- | +| Start | Ready | Running | Start tracking time | +| Start | Stopped | Running | Resume tracking time | +| Stop | Running | Stopped | Stop tracking time | +| Lap | Running | Running | Add current lap to previous laps, then reset current lap | +| Reset | Stopped | Ready | Reset current lap and clear previous laps | diff --git a/exercises/practice/split-second-stopwatch/.docs/introduction.md b/exercises/practice/split-second-stopwatch/.docs/introduction.md new file mode 100644 index 00000000..a8432247 --- /dev/null +++ b/exercises/practice/split-second-stopwatch/.docs/introduction.md @@ -0,0 +1,6 @@ +# Introduction + +You've always run for the thrill of it — no schedules, no timers, just the sound of your feet on the pavement. +But now that you've joined a competitive running crew, things are getting serious. +Training sessions are timed to the second, and every split second counts. +To keep pace, you've picked up the _Split-Second Stopwatch_ — a sleek, high-tech gadget that's about to become your new best friend. diff --git a/exercises/practice/split-second-stopwatch/.meta/config.json b/exercises/practice/split-second-stopwatch/.meta/config.json new file mode 100644 index 00000000..ea9c8777 --- /dev/null +++ b/exercises/practice/split-second-stopwatch/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "keiravillekode" + ], + "files": { + "solution": [ + "split-second-stopwatch/split-second-stopwatch.factor" + ], + "test": [ + "split-second-stopwatch/split-second-stopwatch-tests.factor" + ], + "example": [ + ".meta/example.factor" + ] + }, + "blurb": "Keep track of time through a digital stopwatch.", + "source": "Erik Schierboom", + "source_url": "https://github.com/exercism/problem-specifications/pull/2547" +} diff --git a/exercises/practice/split-second-stopwatch/.meta/example.factor b/exercises/practice/split-second-stopwatch/.meta/example.factor new file mode 100644 index 00000000..4472225b --- /dev/null +++ b/exercises/practice/split-second-stopwatch/.meta/example.factor @@ -0,0 +1,55 @@ +USING: accessors arrays kernel math math.parser sequences splitting ; +IN: split-second-stopwatch + +TUPLE: stopwatch state current history ; + +: ( -- stopwatch ) + stopwatch new + "ready" >>state + 0 >>current + V{ } clone >>history ; + +: parse-time ( str -- seconds ) + ":" split [ string>number ] map first3 + [ 3600 * ] [ 60 * ] [ ] tri* + + ; + +: pad2 ( n -- str ) number>string 2 CHAR: 0 pad-head ; + +: format-time ( seconds -- str ) + [ 3600 /i pad2 ] [ 60 /i 60 mod pad2 ] [ 60 mod pad2 ] tri + ":" glue ":" glue ; + +: state ( stopwatch -- str ) state>> ; + +: current-lap ( stopwatch -- str ) current>> format-time ; + +: total ( stopwatch -- str ) + [ history>> sum ] [ current>> ] bi + format-time ; + +: previous-laps ( stopwatch -- seq ) + history>> [ format-time ] map >array ; + +: start ( stopwatch -- ) + dup state>> "running" = + [ "cannot start an already running stopwatch" throw ] + [ "running" >>state drop ] if ; + +: stop ( stopwatch -- ) + dup state>> "running" = + [ "stopped" >>state drop ] + [ "cannot stop a stopwatch that is not running" throw ] if ; + +: reset ( stopwatch -- ) + dup state>> "stopped" = + [ "ready" >>state 0 >>current V{ } clone >>history drop ] + [ "cannot reset a stopwatch that is not stopped" throw ] if ; + +: lap ( stopwatch -- ) + dup state>> "running" = + [ dup current>> over history>> push 0 >>current drop ] + [ "cannot lap a stopwatch that is not running" throw ] if ; + +: advance-time ( str stopwatch -- ) + dup state>> "running" = + [ [ parse-time ] dip tuck current>> + >>current drop ] + [ 2drop ] if ; diff --git a/exercises/practice/split-second-stopwatch/.meta/generator.jl b/exercises/practice/split-second-stopwatch/.meta/generator.jl new file mode 100644 index 00000000..2a89f0f3 --- /dev/null +++ b/exercises/practice/split-second-stopwatch/.meta/generator.jl @@ -0,0 +1,70 @@ +module SplitSecondStopwatch + +const EXTRA_VOCABS = ["locals"] + +const MUTATION_WORDS = Dict( + "start" => "start", + "stop" => "stop", + "reset" => "reset", + "lap" => "lap", +) + +const QUERY_WORDS = Dict( + "state" => "state", + "currentLap" => "current-lap", + "total" => "total", + "previousLaps" => "previous-laps", +) + +function format_expected(val) + if val isa AbstractString + return "\"$(escape_factor(val))\"" + elseif val isa AbstractVector + return isempty(val) ? "{ }" : format_string_array(val) + else + error("unexpected `expected` value: $val") + end +end + +function is_error_case(commands) + for c in commands + e = get(c, "expected", nothing) + e isa AbstractDict && haskey(e, "error") && return true + end + return false +end + +function gen_test_case(case) + commands = case["input"]["commands"] + body = String[] + expecteds = String[] + for c in commands + name = c["command"] + if name == "new" + push!(body, " :> sw") + elseif name == "advanceTime" + push!(body, "\"$(c["by"])\" sw advance-time") + elseif haskey(MUTATION_WORDS, name) + push!(body, "sw $(MUTATION_WORDS[name])") + elseif haskey(QUERY_WORDS, name) + push!(body, "sw $(QUERY_WORDS[name])") + push!(expecteds, format_expected(c["expected"])) + else + error("unknown command: $name") + end + end + body_str = join(body, "\n ") + if is_error_case(commands) + return """[ + [let $(body_str) + ] +] must-fail""" + else + return """{ $(join(expecteds, " ")) } [ + [let $(body_str) + ] +] unit-test""" + end +end + +end diff --git a/exercises/practice/split-second-stopwatch/.meta/tests.toml b/exercises/practice/split-second-stopwatch/.meta/tests.toml new file mode 100644 index 00000000..323cb7ae --- /dev/null +++ b/exercises/practice/split-second-stopwatch/.meta/tests.toml @@ -0,0 +1,97 @@ +# 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. + +[ddb238ea-99d4-4eaa-a81d-3c917a525a23] +description = "new stopwatch starts in ready state" + +[b19635d4-08ad-4ac3-b87f-aca10e844071] +description = "new stopwatch's current lap has no elapsed time" + +[492eb532-268d-43ea-8a19-2a032067d335] +description = "new stopwatch's total has no elapsed time" + +[8a892c1e-9ef7-4690-894e-e155a1fe4484] +description = "new stopwatch does not have previous laps" + +[5b2705b6-a584-4042-ba3a-4ab8d0ab0281] +description = "start from ready state changes state to running" + +[748235ce-1109-440b-9898-0a431ea179b6] +description = "start does not change previous laps" + +[491487b1-593d-423e-a075-aa78d449ff1f] +description = "start initiates time tracking for current lap" + +[a0a7ba2c-8db6-412c-b1b6-cb890e9b72ed] +description = "start initiates time tracking for total" + +[7f558a17-ef6d-4a5b-803a-f313af7c41d3] +description = "start cannot be called from running state" + +[32466eef-b2be-4d60-a927-e24fce52dab9] +description = "stop from running state changes state to stopped" + +[621eac4c-8f43-4d99-919c-4cad776d93df] +description = "stop pauses time tracking for current lap" + +[465bcc82-7643-41f2-97ff-5e817cef8db4] +description = "stop pauses time tracking for total" + +[b1ba7454-d627-41ee-a078-891b2ed266fc] +description = "stop cannot be called from ready state" + +[5c041078-0898-44dc-9d5b-8ebb5352626c] +description = "stop cannot be called from stopped state" + +[3f32171d-8fbf-46b6-bc2b-0810e1ec53b7] +description = "start from stopped state changes state to running" + +[626997cb-78d5-4fe8-b501-29fdef804799] +description = "start from stopped state resumes time tracking for current lap" + +[58487c53-ab26-471c-a171-807ef6363319] +description = "start from stopped state resumes time tracking for total" + +[091966e3-ed25-4397-908b-8bb0330118f8] +description = "lap adds current lap to previous laps" + +[1aa4c5ee-a7d5-4d59-9679-419deef3c88f] +description = "lap resets current lap and resumes time tracking" + +[4b46b92e-1b3f-46f6-97d2-0082caf56e80] +description = "lap continues time tracking for total" + +[ea75d36e-63eb-4f34-97ce-8c70e620bdba] +description = "lap cannot be called from ready state" + +[63731154-a23a-412d-a13f-c562f208eb1e] +description = "lap cannot be called from stopped state" + +[e585ee15-3b3f-4785-976b-dd96e7cc978b] +description = "stop does not change previous laps" + +[fc3645e2-86cf-4d11-97c6-489f031103f6] +description = "reset from stopped state changes state to ready" + +[20fbfbf7-68ad-4310-975a-f5f132886c4e] +description = "reset resets current lap" + +[00a8f7bb-dd5c-43e5-8705-3ef124007662] +description = "reset clears previous laps" + +[76cea936-6214-4e95-b6d1-4d4edcf90499] +description = "reset cannot be called from ready state" + +[ba4d8e69-f200-4721-b59e-90d8cf615153] +description = "reset cannot be called from running state" + +[0b01751a-cb57-493f-bb86-409de6e84306] +description = "supports very long laps" diff --git a/exercises/practice/split-second-stopwatch/exercism-tools/exercism-tools.factor b/exercises/practice/split-second-stopwatch/exercism-tools/exercism-tools.factor new file mode 100644 index 00000000..a21c60cb --- /dev/null +++ b/exercises/practice/split-second-stopwatch/exercism-tools/exercism-tools.factor @@ -0,0 +1,34 @@ +USING: accessors command-line continuations debugger io kernel + lexer namespaces sequences source-files.errors.debugger + system tools.test vocabs vocabs.loader ; +IN: exercism-tools + +SYNTAX: STOP-HERE + lexer get [ text>> length ] keep line<< ; + +SYNTAX: TASK: + lexer get next-line ; + +! Label the test that follows with its description. +: description ( str -- ) + "###DESC### " write print ; + +! Print one failure block in a stable, parser-friendly form. +:: print-failure ( failure -- ) + "###FAIL_BEGIN###" print + failure error-location print + failure error>> [ error. ] [ 2drop ] recover + "###FAIL_END###" print + flush ; + +: print-failures ( -- ) + test-failures get [ print-failure ] each ; + +: run-exercism-tests ( -- ) + vocab-roots [ "." prefix ] change-global + command-line get first + [ require ] [ test ] bi + test-failures get empty? + [ 0 exit ] [ print-failures 1 exit ] if ; + +MAIN: run-exercism-tests diff --git a/exercises/practice/split-second-stopwatch/split-second-stopwatch/split-second-stopwatch-tests.factor b/exercises/practice/split-second-stopwatch/split-second-stopwatch/split-second-stopwatch-tests.factor new file mode 100644 index 00000000..d008856c --- /dev/null +++ b/exercises/practice/split-second-stopwatch/split-second-stopwatch/split-second-stopwatch-tests.factor @@ -0,0 +1,293 @@ +USING: exercism-tools io kernel locals split-second-stopwatch tools.test unicode ; +IN: split-second-stopwatch.tests + +"new stopwatch starts in ready state" description +{ "ready" } [ + [let :> sw + sw state + ] +] unit-test + +STOP-HERE + +"new stopwatch's current lap has no elapsed time" description +{ "00:00:00" } [ + [let :> sw + sw current-lap + ] +] unit-test + +"new stopwatch's total has no elapsed time" description +{ "00:00:00" } [ + [let :> sw + sw total + ] +] unit-test + +"new stopwatch does not have previous laps" description +{ { } } [ + [let :> sw + sw previous-laps + ] +] unit-test + +"start from ready state changes state to running" description +{ "running" } [ + [let :> sw + sw start + sw state + ] +] unit-test + +"start does not change previous laps" description +{ { } } [ + [let :> sw + sw start + sw previous-laps + ] +] unit-test + +"start initiates time tracking for current lap" description +{ "00:00:05" } [ + [let :> sw + sw start + "00:00:05" sw advance-time + sw current-lap + ] +] unit-test + +"start initiates time tracking for total" description +{ "00:00:23" } [ + [let :> sw + sw start + "00:00:23" sw advance-time + sw total + ] +] unit-test + +"start cannot be called from running state" description +[ + [let :> sw + sw start + sw start + ] +] must-fail + +"stop from running state changes state to stopped" description +{ "stopped" } [ + [let :> sw + sw start + sw stop + sw state + ] +] unit-test + +"stop pauses time tracking for current lap" description +{ "00:00:05" } [ + [let :> sw + sw start + "00:00:05" sw advance-time + sw stop + "00:00:08" sw advance-time + sw current-lap + ] +] unit-test + +"stop pauses time tracking for total" description +{ "00:00:13" } [ + [let :> sw + sw start + "00:00:13" sw advance-time + sw stop + "00:00:44" sw advance-time + sw total + ] +] unit-test + +"stop cannot be called from ready state" description +[ + [let :> sw + sw stop + ] +] must-fail + +"stop cannot be called from stopped state" description +[ + [let :> sw + sw start + sw stop + sw stop + ] +] must-fail + +"start from stopped state changes state to running" description +{ "running" } [ + [let :> sw + sw start + sw stop + sw start + sw state + ] +] unit-test + +"start from stopped state resumes time tracking for current lap" description +{ "00:01:28" } [ + [let :> sw + sw start + "00:01:20" sw advance-time + sw stop + "00:00:20" sw advance-time + sw start + "00:00:08" sw advance-time + sw current-lap + ] +] unit-test + +"start from stopped state resumes time tracking for total" description +{ "00:00:32" } [ + [let :> sw + sw start + "00:00:23" sw advance-time + sw stop + "00:00:44" sw advance-time + sw start + "00:00:09" sw advance-time + sw total + ] +] unit-test + +"lap adds current lap to previous laps" description +{ { "00:01:38" } { "00:01:38" "00:00:44" } } [ + [let :> sw + sw start + "00:01:38" sw advance-time + sw lap + sw previous-laps + "00:00:44" sw advance-time + sw lap + sw previous-laps + ] +] unit-test + +"lap resets current lap and resumes time tracking" description +{ "00:00:00" "00:00:15" } [ + [let :> sw + sw start + "00:08:22" sw advance-time + sw lap + sw current-lap + "00:00:15" sw advance-time + sw current-lap + ] +] unit-test + +"lap continues time tracking for total" description +{ "00:00:55" } [ + [let :> sw + sw start + "00:00:22" sw advance-time + sw lap + "00:00:33" sw advance-time + sw total + ] +] unit-test + +"lap cannot be called from ready state" description +[ + [let :> sw + sw lap + ] +] must-fail + +"lap cannot be called from stopped state" description +[ + [let :> sw + sw start + sw stop + sw lap + ] +] must-fail + +"stop does not change previous laps" description +{ { "00:11:22" } { "00:11:22" } } [ + [let :> sw + sw start + "00:11:22" sw advance-time + sw lap + sw previous-laps + sw stop + sw previous-laps + ] +] unit-test + +"reset from stopped state changes state to ready" description +{ "ready" } [ + [let :> sw + sw start + sw stop + sw reset + sw state + ] +] unit-test + +"reset resets current lap" description +{ "00:00:00" } [ + [let :> sw + sw start + "00:00:10" sw advance-time + sw stop + sw reset + sw current-lap + ] +] unit-test + +"reset clears previous laps" description +{ { "00:00:10" "00:00:20" } { } } [ + [let :> sw + sw start + "00:00:10" sw advance-time + sw lap + "00:00:20" sw advance-time + sw lap + sw previous-laps + sw stop + sw reset + sw previous-laps + ] +] unit-test + +"reset cannot be called from ready state" description +[ + [let :> sw + sw reset + ] +] must-fail + +"reset cannot be called from running state" description +[ + [let :> sw + sw start + sw reset + ] +] must-fail + +"supports very long laps" description +{ "01:23:45" { "01:23:45" } "04:01:40" "05:25:25" { "01:23:45" "04:01:40" } "08:43:05" "14:08:30" { "01:23:45" "04:01:40" "08:43:05" } } [ + [let :> sw + sw start + "01:23:45" sw advance-time + sw current-lap + sw lap + sw previous-laps + "04:01:40" sw advance-time + sw current-lap + sw total + sw lap + sw previous-laps + "08:43:05" sw advance-time + sw current-lap + sw total + sw lap + sw previous-laps + ] +] unit-test diff --git a/exercises/practice/split-second-stopwatch/split-second-stopwatch/split-second-stopwatch.factor b/exercises/practice/split-second-stopwatch/split-second-stopwatch/split-second-stopwatch.factor new file mode 100644 index 00000000..0ec156c9 --- /dev/null +++ b/exercises/practice/split-second-stopwatch/split-second-stopwatch/split-second-stopwatch.factor @@ -0,0 +1,45 @@ +USING: kernel ; +IN: split-second-stopwatch + +! Define a `stopwatch` tuple to hold the current state, the +! current lap's elapsed seconds, and the previously recorded +! laps. `` should return a new stopwatch in the +! "ready" state. + +! `state` returns "ready", "running", or "stopped". +! `current-lap` and `total` return "HH:MM:SS" strings. +! `previous-laps` returns an array of "HH:MM:SS" strings. +! `advance-time` takes an "HH:MM:SS" string and adds it to the +! current lap (only while running). +! `start`, `stop`, `reset`, and `lap` change the state, throwing +! when called from a state that does not allow them. + +: ( -- stopwatch ) + "unimplemented" throw ; + +: state ( stopwatch -- str ) + "unimplemented" throw ; + +: current-lap ( stopwatch -- str ) + "unimplemented" throw ; + +: total ( stopwatch -- str ) + "unimplemented" throw ; + +: previous-laps ( stopwatch -- seq ) + "unimplemented" throw ; + +: advance-time ( str stopwatch -- ) + "unimplemented" throw ; + +: start ( stopwatch -- ) + "unimplemented" throw ; + +: stop ( stopwatch -- ) + "unimplemented" throw ; + +: reset ( stopwatch -- ) + "unimplemented" throw ; + +: lap ( stopwatch -- ) + "unimplemented" throw ;