From 62e503df9fdd550e39449b709aab163c7a97a63d Mon Sep 17 00:00:00 2001 From: Eric Willigers Date: Sat, 25 Apr 2026 10:19:52 +1000 Subject: [PATCH 1/2] currency-exchange cars-assemble log-levels We add three concept exercises, covering mathematics, `cond` and string handling. --- config.json | 38 ++++++++ .../concept/cars-assemble/.docs/hints.md | 35 +++++++ .../cars-assemble/.docs/instructions.md | 55 +++++++++++ .../cars-assemble/.docs/introduction.md | 81 ++++++++++++++++ .../concept/cars-assemble/.docs/source.md | 5 + .../concept/cars-assemble/.meta/config.json | 20 ++++ .../concept/cars-assemble/.meta/design.md | 31 ++++++ .../cars-assemble/.meta/exemplar.factor | 19 ++++ .../cars-assemble/cars-assemble-tests.factor | 25 +++++ .../cars-assemble/cars-assemble.factor | 13 +++ .../concept/currency-exchange/.docs/hints.md | 46 +++++++++ .../currency-exchange/.docs/instructions.md | 83 ++++++++++++++++ .../currency-exchange/.docs/introduction.md | 56 +++++++++++ .../concept/currency-exchange/.docs/source.md | 5 + .../currency-exchange/.meta/config.json | 20 ++++ .../concept/currency-exchange/.meta/design.md | 26 +++++ .../currency-exchange/.meta/exemplar.factor | 24 +++++ .../currency-exchange-tests.factor | 32 +++++++ .../currency-exchange.factor | 20 ++++ exercises/concept/log-levels/.docs/hints.md | 33 +++++++ .../concept/log-levels/.docs/instructions.md | 46 +++++++++ .../concept/log-levels/.docs/introduction.md | 95 +++++++++++++++++++ exercises/concept/log-levels/.docs/source.md | 5 + .../concept/log-levels/.meta/config.json | 20 ++++ exercises/concept/log-levels/.meta/design.md | 32 +++++++ .../concept/log-levels/.meta/exemplar.factor | 11 +++ .../log-levels/log-levels-tests.factor | 18 ++++ .../log-levels/log-levels/log-levels.factor | 11 +++ 28 files changed, 905 insertions(+) create mode 100644 exercises/concept/cars-assemble/.docs/hints.md create mode 100644 exercises/concept/cars-assemble/.docs/instructions.md create mode 100644 exercises/concept/cars-assemble/.docs/introduction.md create mode 100644 exercises/concept/cars-assemble/.docs/source.md create mode 100644 exercises/concept/cars-assemble/.meta/config.json create mode 100644 exercises/concept/cars-assemble/.meta/design.md create mode 100644 exercises/concept/cars-assemble/.meta/exemplar.factor create mode 100644 exercises/concept/cars-assemble/cars-assemble/cars-assemble-tests.factor create mode 100644 exercises/concept/cars-assemble/cars-assemble/cars-assemble.factor create mode 100644 exercises/concept/currency-exchange/.docs/hints.md create mode 100644 exercises/concept/currency-exchange/.docs/instructions.md create mode 100644 exercises/concept/currency-exchange/.docs/introduction.md create mode 100644 exercises/concept/currency-exchange/.docs/source.md create mode 100644 exercises/concept/currency-exchange/.meta/config.json create mode 100644 exercises/concept/currency-exchange/.meta/design.md create mode 100644 exercises/concept/currency-exchange/.meta/exemplar.factor create mode 100644 exercises/concept/currency-exchange/currency-exchange/currency-exchange-tests.factor create mode 100644 exercises/concept/currency-exchange/currency-exchange/currency-exchange.factor create mode 100644 exercises/concept/log-levels/.docs/hints.md create mode 100644 exercises/concept/log-levels/.docs/instructions.md create mode 100644 exercises/concept/log-levels/.docs/introduction.md create mode 100644 exercises/concept/log-levels/.docs/source.md create mode 100644 exercises/concept/log-levels/.meta/config.json create mode 100644 exercises/concept/log-levels/.meta/design.md create mode 100644 exercises/concept/log-levels/.meta/exemplar.factor create mode 100644 exercises/concept/log-levels/log-levels/log-levels-tests.factor create mode 100644 exercises/concept/log-levels/log-levels/log-levels.factor diff --git a/config.json b/config.json index b8d0f9e5..414cf5f4 100644 --- a/config.json +++ b/config.json @@ -52,6 +52,44 @@ "stack-effect" ], "status": "wip" + }, + { + "slug": "currency-exchange", + "name": "Currency Exchange", + "uuid": "380a2d4f-ea48-47a1-94f1-fc8ec0561136", + "concepts": [ + "numbers" + ], + "prerequisites": [ + "stack-effect" + ], + "status": "wip" + }, + { + "slug": "cars-assemble", + "name": "Cars, Assemble!", + "uuid": "e5e68b1a-5445-4251-a56b-6a40fab662ed", + "concepts": [ + "conditionals" + ], + "prerequisites": [ + "booleans", + "numbers" + ], + "status": "wip" + }, + { + "slug": "log-levels", + "name": "Log Levels", + "uuid": "2e436217-a007-438e-b553-451e10d9c459", + "concepts": [ + "strings" + ], + "prerequisites": [ + "stack-effect", + "conditionals" + ], + "status": "wip" } ], "practice": [ diff --git a/exercises/concept/cars-assemble/.docs/hints.md b/exercises/concept/cars-assemble/.docs/hints.md new file mode 100644 index 00000000..23fa62a4 --- /dev/null +++ b/exercises/concept/cars-assemble/.docs/hints.md @@ -0,0 +1,35 @@ +# Hints + +## General + +- `cond` in [`combinators`][combinators] is the cleanest way to map a + speed to a success rate. +- The arithmetic words live in [`math`][math]; the float-to-integer + helpers `floor` and `>integer` live in + [`math.functions`][math.functions] and `math` respectively. + +## 1. Calculate the success rate + +- A `cond` clause is `{ [ predicate ] [ body ] }`. Each predicate + inspects the speed but should leave the stack as it found it + (a `dup` … `` does that). The body usually starts with `drop`. +- Order matters: check `zero?` before `4 <=`, because `0 4 <=` is `t`. +- Use a final entry without a predicate as the default for speed `10`. + +## 2. Calculate the production rate per hour + +- Define `base-speed` as a `CONSTANT:` near the top of the file (just + like `expected-bake-time` in `lasagna`). +- Compute `base-speed * speed * success-rate`. One option is to use + `bi` from `kernel`: it runs two quotations on the same input and + leaves both results on the stack. + +## 3. Calculate the number of working items produced per minute + +- Divide the hourly rate by 60. +- Use `floor` to drop the fractional part, then `>integer` to convert + the float result into an integer. + +[math]: https://docs.factorcode.org/content/vocab-math.html +[math.functions]: https://docs.factorcode.org/content/vocab-math.functions.html +[combinators]: https://docs.factorcode.org/content/vocab-combinators.html diff --git a/exercises/concept/cars-assemble/.docs/instructions.md b/exercises/concept/cars-assemble/.docs/instructions.md new file mode 100644 index 00000000..5aa340b3 --- /dev/null +++ b/exercises/concept/cars-assemble/.docs/instructions.md @@ -0,0 +1,55 @@ +# Instructions + +In this exercise you'll be writing code to analyze the production of +an assembly line in a car factory. The assembly line's speed can range +from `0` (off) to `10` (maximum). + +At its lowest non-zero speed (`1`), `221` cars are produced each hour. +The production increases linearly with the speed, so at speed `4` the +line produces `4 * 221 = 884` cars per hour. However, higher speeds +increase the likelihood that faulty cars are produced, which then +have to be discarded. + +You have three tasks. Each takes a single integer parameter — the +speed of the assembly line — off the stack. + +## 1. Calculate the success rate + +Define `success-rate` to return the probability of an item being +produced without error: + +- `0`: `0.0` +- `1` to `4`: `1.0` +- `5` to `8`: `0.9` +- `9`: `0.8` +- `10`: `0.77` + +```factor +10 success-rate . +! => 0.77 +``` + +## 2. Calculate the production rate per hour + +Define `production-rate-per-hour` to return the assembly line's +production rate per hour, taking the success rate into account. + +You'll need to define `base-speed` first, the constant `221`. + +```factor +6 production-rate-per-hour . +! => 1193.4 +``` + +The value returned is floating-point. + +## 3. Calculate the number of working items produced per minute + +Define `working-items-per-minute` to return how many working cars are +produced per minute. The result is an integer — partial cars are not +counted. + +```factor +6 working-items-per-minute . +! => 19 +``` diff --git a/exercises/concept/cars-assemble/.docs/introduction.md b/exercises/concept/cars-assemble/.docs/introduction.md new file mode 100644 index 00000000..4fe46e62 --- /dev/null +++ b/exercises/concept/cars-assemble/.docs/introduction.md @@ -0,0 +1,81 @@ +# Introduction + +This exercise introduces conditionals — choosing between two or more +courses of action based on a value. It builds on the booleans you met +in [Annalyn's Infiltration][annalyns] and the integer arithmetic from +[Currency Exchange][currency-exchange]. + +## Comparison words + +These all live in [`math`][math] (and `kernel` for `=`): + +``` += ( x y -- ? ) ! equal +< ( x y -- ? ) ! less than +<= ( x y -- ? ) ! less than or equal +> ( x y -- ? ) ! greater than +>= ( x y -- ? ) ! greater than or equal +``` + +```factor +3 3 = . ! => t +2 3 < . ! => t +3 3 <= . ! => t +``` + +## `if`, `when`, `unless` + +`if` (in [`kernel`][kernel]) takes a boolean and two quotations. It +runs the first quotation when the boolean is truthy and the second +when it is falsy: + +``` +if ( ? then-quot else-quot -- ) +``` + +```factor +: abs ( x -- y ) dup 0 < [ neg ] [ ] if ; +``` + +`when` runs its quotation only when the boolean is truthy; `unless` +only when it is falsy: + +``` +when ( ? quot -- ) +unless ( ? quot -- ) +``` + +## `cond` + +When you have several alternative actions to choose between, `cond` +(in [`combinators`][combinators]) is the natural fit. It takes an +array of `{ predicate body }` pairs and runs the body of the first +predicate that yields a truthy value: + +```factor +USING: combinators ; + +: classify ( n -- label ) + { + { [ dup 0 < ] [ drop "negative" ] } + { [ dup 0 = ] [ drop "zero" ] } + [ drop "positive" ] + } cond ; +``` + +A few details worth noting: + +- The pairs are tried in order. The first match wins. +- An entry without a predicate (just a single quotation) at the end + acts as the default. +- Each predicate inspects the input but should leave the data stack + the way it found it — `dup ... ` is the usual idiom. +- The body of the chosen pair receives the same stack the predicate + saw, so it usually starts by `drop`ping the input and pushing the + result. + +[annalyns]: https://exercism.org/tracks/factor/exercises/annalyns-infiltration +[currency-exchange]: https://exercism.org/tracks/factor/exercises/currency-exchange +[math]: https://docs.factorcode.org/content/vocab-math.html +[kernel]: https://docs.factorcode.org/content/vocab-kernel.html +[combinators]: https://docs.factorcode.org/content/vocab-combinators.html diff --git a/exercises/concept/cars-assemble/.docs/source.md b/exercises/concept/cars-assemble/.docs/source.md new file mode 100644 index 00000000..d0aa1a31 --- /dev/null +++ b/exercises/concept/cars-assemble/.docs/source.md @@ -0,0 +1,5 @@ +# Source + +Forked from the Julia track's ["Cars, Assemble!" exercise][julia-source]. + +[julia-source]: https://github.com/exercism/julia/tree/main/exercises/concept/cars-assemble diff --git a/exercises/concept/cars-assemble/.meta/config.json b/exercises/concept/cars-assemble/.meta/config.json new file mode 100644 index 00000000..9cfa46c9 --- /dev/null +++ b/exercises/concept/cars-assemble/.meta/config.json @@ -0,0 +1,20 @@ +{ + "authors": [ + "keiravillekode" + ], + "files": { + "solution": [ + "cars-assemble/cars-assemble.factor" + ], + "test": [ + "cars-assemble/cars-assemble-tests.factor" + ], + "exemplar": [ + ".meta/exemplar.factor" + ] + }, + "forked_from": [ + "julia/cars-assemble" + ], + "blurb": "Learn about conditionals in Factor by analyzing the production of an assembly line." +} diff --git a/exercises/concept/cars-assemble/.meta/design.md b/exercises/concept/cars-assemble/.meta/design.md new file mode 100644 index 00000000..8ab404aa --- /dev/null +++ b/exercises/concept/cars-assemble/.meta/design.md @@ -0,0 +1,31 @@ +# Design + +## Goal + +Introduce Factor's conditional combinators by mapping a discrete input +(speed) onto a discrete output (success rate). + +## Learning objectives + +- Use comparison words `=`, `<`, `<=`, `>`, `>=` from `math` and `kernel`. +- Use `if`, `when`, `unless` to choose between two quotations. +- Use `cond` from `combinators` for a chained "first matching predicate" + selection. +- Combine the `dup` … `drop` pattern with `cond` to inspect a value + without consuming it. + +## Out of scope + +- `case` from `combinators` for value-based dispatch. +- Multimethods, `GENERIC:`. +- Arithmetic combinators like `bi` are introduced incidentally for + task 2; deeper coverage belongs in a later exercise. + +## Concepts + +- `conditionals`: `if`, `when`, `unless`, and especially `cond`. + +## Prerequisites + +- `booleans` — taught in `annalyns-infiltration`. +- `numbers` — taught in `currency-exchange`. diff --git a/exercises/concept/cars-assemble/.meta/exemplar.factor b/exercises/concept/cars-assemble/.meta/exemplar.factor new file mode 100644 index 00000000..e20a96d9 --- /dev/null +++ b/exercises/concept/cars-assemble/.meta/exemplar.factor @@ -0,0 +1,19 @@ +USING: combinators kernel math math.functions ; +IN: cars-assemble + +CONSTANT: base-speed 221 + +: success-rate ( speed -- rate ) + { + { [ dup zero? ] [ drop 0.0 ] } + { [ dup 4 <= ] [ drop 1.0 ] } + { [ dup 8 <= ] [ drop 0.9 ] } + { [ dup 9 = ] [ drop 0.8 ] } + [ drop 0.77 ] + } cond ; + +: production-rate-per-hour ( speed -- rate ) + [ base-speed * ] [ success-rate ] bi * ; + +: working-items-per-minute ( speed -- count ) + production-rate-per-hour 60 / floor >integer ; diff --git a/exercises/concept/cars-assemble/cars-assemble/cars-assemble-tests.factor b/exercises/concept/cars-assemble/cars-assemble/cars-assemble-tests.factor new file mode 100644 index 00000000..7699e047 --- /dev/null +++ b/exercises/concept/cars-assemble/cars-assemble/cars-assemble-tests.factor @@ -0,0 +1,25 @@ +USING: cars-assemble tools.test ; +IN: cars-assemble.tests + +! TASK: 1 success-rate +{ 0.0 } [ 0 success-rate ] unit-test +{ 1.0 } [ 1 success-rate ] unit-test +{ 1.0 } [ 4 success-rate ] unit-test +{ 0.9 } [ 5 success-rate ] unit-test +{ 0.8 } [ 9 success-rate ] unit-test +{ 0.77 } [ 10 success-rate ] unit-test + +! TASK: 2 production-rate-per-hour +{ 0.0 } [ 0 production-rate-per-hour ] unit-test +{ 221.0 } [ 1 production-rate-per-hour ] unit-test +{ 884.0 } [ 4 production-rate-per-hour ] unit-test +{ 994.5 } [ 5 production-rate-per-hour ] unit-test +{ 1193.4 } [ 6 production-rate-per-hour ] unit-test +{ 1591.2 } [ 9 production-rate-per-hour ] unit-test +{ 1701.7 } [ 10 production-rate-per-hour ] unit-test + +! TASK: 3 working-items-per-minute +{ 0 } [ 0 working-items-per-minute ] unit-test +{ 14 } [ 4 working-items-per-minute ] unit-test +{ 19 } [ 6 working-items-per-minute ] unit-test +{ 28 } [ 10 working-items-per-minute ] unit-test diff --git a/exercises/concept/cars-assemble/cars-assemble/cars-assemble.factor b/exercises/concept/cars-assemble/cars-assemble/cars-assemble.factor new file mode 100644 index 00000000..0a74b5c1 --- /dev/null +++ b/exercises/concept/cars-assemble/cars-assemble/cars-assemble.factor @@ -0,0 +1,13 @@ +USING: kernel ; +IN: cars-assemble + +! Define base-speed. + +: success-rate ( speed -- rate ) + "unimplemented" throw ; + +: production-rate-per-hour ( speed -- rate ) + "unimplemented" throw ; + +: working-items-per-minute ( speed -- count ) + "unimplemented" throw ; diff --git a/exercises/concept/currency-exchange/.docs/hints.md b/exercises/concept/currency-exchange/.docs/hints.md new file mode 100644 index 00000000..e15b6925 --- /dev/null +++ b/exercises/concept/currency-exchange/.docs/hints.md @@ -0,0 +1,46 @@ +# Hints + +## General + +- All the words you need live in the [`math`][math] and + [`math.functions`][math.functions] vocabularies. Add them to your + `USING:` line. + +## 1. Estimate value after exchange + +- Use `/` (stack effect `( x y -- x/y )`) to divide budget by rate. + +## 2. Calculate currency left after an exchange + +- Use `-` to subtract. + +## 3. Calculate value of bills + +- Use `*` to multiply. + +## 4. Calculate number of bills + +- The `amount` may be a float; you need an integer result. +- One approach: convert the amount to an integer first with + `floor >integer`, then divide with `/i`. +- An alternative: use `/i` directly — for an integer denominator it + also truncates the float — and then convert with `>integer`. + +## 5. Calculate leftover after exchanging into bills + +- Use `mod` for the remainder. + +## 6. Calculate value after exchange + +- Compute the *total* exchange rate: take `spread` percent of + `exchange-rate` and add it to `exchange-rate`. +- Then call `exchange-money` with the budget and total rate. +- Then call `number-of-bills` to round down to a whole number of + bills of the given denomination. +- Then call `value-of-bills` to get the final value. +- The four sub-calculations chain naturally; if your stack juggling + starts to feel awkward, the locals form (`::` with named parameters) + is fine here. + +[math]: https://docs.factorcode.org/content/vocab-math.html +[math.functions]: https://docs.factorcode.org/content/vocab-math.functions.html diff --git a/exercises/concept/currency-exchange/.docs/instructions.md b/exercises/concept/currency-exchange/.docs/instructions.md new file mode 100644 index 00000000..77870ed8 --- /dev/null +++ b/exercises/concept/currency-exchange/.docs/instructions.md @@ -0,0 +1,83 @@ +# Instructions + +Your friend Chandler plans to visit exotic countries all around the +world. Sadly, Chandler's math skills aren't good. He's pretty worried +about being scammed by currency exchanges during his trip — and he +wants you to make a currency calculator for him. + +## 1. Estimate value after exchange + +Define `exchange-money` taking a `budget` and an `exchange-rate` and +returning the value of the exchanged currency. + +**Note:** if your currency is USD and you want to exchange USD for EUR +with an exchange rate of `1.20`, then `1.20 USD == 1 EUR`. + +```factor +127.5 1.2 exchange-money . +! => 106.25 +``` + +## 2. Calculate currency left after an exchange + +Define `get-change` taking a `budget` (before the exchange) and the +`exchanging-value` (the amount taken from the budget to be exchanged). +Return the amount of money that is left. + +```factor +127.5 120 get-change . +! => 7.5 +``` + +## 3. Calculate value of bills + +Define `value-of-bills` taking a `denomination` (the value of a single +bill) and a `number-of-bills`. Return the total value of the bills. + +```factor +5 128 value-of-bills . +! => 640 +``` + +## 4. Calculate number of bills + +Define `number-of-bills` taking an `amount` and a `denomination`. +Return the number of *whole bills* of the given denomination that fit +into the amount. Round down — fractional bills don't exist. + +```factor +127.5 5 number-of-bills . +! => 25 +``` + +## 5. Calculate leftover after exchanging into bills + +Define `leftover-of-bills` taking an `amount` and a `denomination`. +Return the leftover amount that cannot be returned as whole bills. + +```factor +127.5 20 leftover-of-bills . +! => 7.5 +``` + +## 6. Calculate value after exchange + +Define `exchangeable-value` taking a `budget`, `exchange-rate`, +`spread`, and `denomination`. + +`spread` is the *percentage* taken as an exchange fee, written as an +integer. It needs to be added to the exchange rate as a fraction. If +`1.00 EUR == 1.20 USD` and the spread is `10`, the total exchange rate +is `1.00 EUR == 1.32 USD` (10% of 1.20 is 0.12, added to 1.20). + +Return the maximum value of the new currency after applying the rate +plus spread, rounded down to whole bills of the given `denomination`. +The returned value is an integer. + +```factor +127.25 1.20 10 20 exchangeable-value . +! => 80 + +127.25 1.20 10 5 exchangeable-value . +! => 95 +``` diff --git a/exercises/concept/currency-exchange/.docs/introduction.md b/exercises/concept/currency-exchange/.docs/introduction.md new file mode 100644 index 00000000..e5d8a470 --- /dev/null +++ b/exercises/concept/currency-exchange/.docs/introduction.md @@ -0,0 +1,56 @@ +# Introduction + +This exercise builds on the integer arithmetic from +[Leah's Luscious Lasagna][lasagna]. Here you will work with floating- +point numbers and pick the right division word for the job. + +## Integers and floats + +Factor distinguishes integers (`1`, `42`, `1_000_000`) from floating- +point numbers (`1.0`, `3.14`, `6.02e23`). Mixed arithmetic auto- +promotes to a float: + +```factor +2 3 + . ! => 5 +2 3.0 + . ! => 5.0 +``` + +Underscores are allowed inside number literals as digit separators +and are ignored by the parser. + +## Division words + +Factor has separate division words. They all live in the [`math`][math] +vocabulary. + +``` +/ ( x y -- x/y ) ! float (or rational) result +/i ( x y -- q ) ! integer division (truncates toward negative infinity) +mod ( x y -- r ) ! remainder +``` + +```factor +10 3 / . ! => 10/3 (a rational; print as a fraction) +10 3 /i . ! => 3 (truncated) +10 3 mod . ! => 1 +10.0 3 / . ! => 3.3333333333333335 +``` + +`/` produces a `ratio` (Factor's exact rational type) for two integers, +or a `float` if either input is a float. `/i` always rounds toward +negative infinity. + +## Float to integer + +`floor`, `ceil`, and `round` from the +[`math.functions`][math.functions] vocabulary all return a float +(`3.0`, not `3`). To get a true integer, chain `>integer`: + +```factor +3.7 floor >integer . ! => 3 +3.2 ceil >integer . ! => 4 +``` + +[lasagna]: https://exercism.org/tracks/factor/exercises/lasagna +[math]: https://docs.factorcode.org/content/vocab-math.html +[math.functions]: https://docs.factorcode.org/content/vocab-math.functions.html diff --git a/exercises/concept/currency-exchange/.docs/source.md b/exercises/concept/currency-exchange/.docs/source.md new file mode 100644 index 00000000..7224adcd --- /dev/null +++ b/exercises/concept/currency-exchange/.docs/source.md @@ -0,0 +1,5 @@ +# Source + +Forked from the Julia track's ["Currency Exchange" exercise][julia-source]. + +[julia-source]: https://github.com/exercism/julia/tree/main/exercises/concept/currency-exchange diff --git a/exercises/concept/currency-exchange/.meta/config.json b/exercises/concept/currency-exchange/.meta/config.json new file mode 100644 index 00000000..893ad928 --- /dev/null +++ b/exercises/concept/currency-exchange/.meta/config.json @@ -0,0 +1,20 @@ +{ + "authors": [ + "keiravillekode" + ], + "files": { + "solution": [ + "currency-exchange/currency-exchange.factor" + ], + "test": [ + "currency-exchange/currency-exchange-tests.factor" + ], + "exemplar": [ + ".meta/exemplar.factor" + ] + }, + "forked_from": [ + "julia/currency-exchange" + ], + "blurb": "Learn about numbers in Factor by solving Chandler's currency exchange conundrums." +} diff --git a/exercises/concept/currency-exchange/.meta/design.md b/exercises/concept/currency-exchange/.meta/design.md new file mode 100644 index 00000000..64e65725 --- /dev/null +++ b/exercises/concept/currency-exchange/.meta/design.md @@ -0,0 +1,26 @@ +# Design + +## Goal + +Introduce floating-point numbers, integer division, and remainder, building on the integer arithmetic already met in `lasagna`. + +## Learning objectives + +- Use the floating-point division word `/`. +- Use the integer division word `/i` and the remainder word `mod`. +- Use the floor word from `math.functions` together with `>integer` to produce an integer from a float. +- Compose previously-defined words inside a new word. + +## Out of scope + +- Rationals (Factor's `ratio` type). +- The full `math.functions` API (only `floor` is needed). +- Float precision and `unit-test~` tolerances (the test file uses `unit-test~` where exact comparison would fail; the student can use plain `unit-test` in their own experiments). + +## Concepts + +- `numbers`: floats vs integers, `/` vs `/i`, `mod`, `floor` plus `>integer`. + +## Prerequisites + +- `stack-effect` — taught in `lasagna`. diff --git a/exercises/concept/currency-exchange/.meta/exemplar.factor b/exercises/concept/currency-exchange/.meta/exemplar.factor new file mode 100644 index 00000000..eebbec0c --- /dev/null +++ b/exercises/concept/currency-exchange/.meta/exemplar.factor @@ -0,0 +1,24 @@ +USING: kernel locals math math.functions ; +IN: currency-exchange + +: exchange-money ( budget exchange-rate -- exchanged ) + / ; + +: get-change ( budget exchanging-value -- change ) + - ; + +: value-of-bills ( denomination number-of-bills -- value ) + * ; + +: number-of-bills ( amount denomination -- bills ) + [ floor >integer ] dip /i ; + +: leftover-of-bills ( amount denomination -- leftover ) + mod ; + +:: exchangeable-value ( budget exchange-rate spread denomination -- value ) + budget + exchange-rate exchange-rate 100 / spread * + + exchange-money + denomination number-of-bills + denomination swap value-of-bills ; diff --git a/exercises/concept/currency-exchange/currency-exchange/currency-exchange-tests.factor b/exercises/concept/currency-exchange/currency-exchange/currency-exchange-tests.factor new file mode 100644 index 00000000..138d7907 --- /dev/null +++ b/exercises/concept/currency-exchange/currency-exchange/currency-exchange-tests.factor @@ -0,0 +1,32 @@ +USING: currency-exchange tools.test ; +IN: currency-exchange.tests + +! TASK: 1 exchange-money +{ 125000.0 } [ 100000 0.8 exchange-money ] unit-test +{ 70000.0 } [ 700000 10.0 exchange-money ] unit-test + +! TASK: 2 get-change +{ 458000 } [ 463000 5000 get-change ] unit-test +{ 1130 } [ 1250 120 get-change ] unit-test +{ 13620 } [ 15000 1380 get-change ] unit-test + +! TASK: 3 value-of-bills +{ 1280000 } [ 10000 128 value-of-bills ] unit-test +{ 18000 } [ 50 360 value-of-bills ] unit-test +{ 40000 } [ 200 200 value-of-bills ] unit-test + +! TASK: 4 number-of-bills +{ 3 } [ 163270 50000 number-of-bills ] unit-test +{ 54 } [ 54361 1000 number-of-bills ] unit-test + +! TASK: 5 leftover-of-bills +{ 0.1 1e-8 } [ 10.1 10 leftover-of-bills ] unit-test~ +{ 1.0 1e-8 } [ 654321.0 5 leftover-of-bills ] unit-test~ +{ 1.14 1e-8 } [ 3.14 2 leftover-of-bills ] unit-test~ + +! TASK: 6 exchangeable-value +{ 8568 } [ 100000 10.61 10 1 exchangeable-value ] unit-test +{ 1400 } [ 1500 0.84 25 40 exchangeable-value ] unit-test +{ 0 } [ 470000 1050 30 10000000000 exchangeable-value ] unit-test +{ 4017094016600 } [ 470000 0.00000009 30 700 exchangeable-value ] unit-test +{ 363300 } [ 425.33 0.0009 30 700 exchangeable-value ] unit-test diff --git a/exercises/concept/currency-exchange/currency-exchange/currency-exchange.factor b/exercises/concept/currency-exchange/currency-exchange/currency-exchange.factor new file mode 100644 index 00000000..ab016859 --- /dev/null +++ b/exercises/concept/currency-exchange/currency-exchange/currency-exchange.factor @@ -0,0 +1,20 @@ +USING: kernel ; +IN: currency-exchange + +: exchange-money ( budget exchange-rate -- exchanged ) + "unimplemented" throw ; + +: get-change ( budget exchanging-value -- change ) + "unimplemented" throw ; + +: value-of-bills ( denomination number-of-bills -- value ) + "unimplemented" throw ; + +: number-of-bills ( amount denomination -- bills ) + "unimplemented" throw ; + +: leftover-of-bills ( amount denomination -- leftover ) + "unimplemented" throw ; + +: exchangeable-value ( budget exchange-rate spread denomination -- value ) + "unimplemented" throw ; diff --git a/exercises/concept/log-levels/.docs/hints.md b/exercises/concept/log-levels/.docs/hints.md new file mode 100644 index 00000000..94c6e88d --- /dev/null +++ b/exercises/concept/log-levels/.docs/hints.md @@ -0,0 +1,33 @@ +# Hints + +## General + +- All three tasks need to break the log line apart. The + [`splitting`][splitting] vocabulary is the right starting point. +- For the trim helper you'll need `blank?` from `ascii` and `trim` + from [`sequences`][sequences]. + +## 1. Get message from a log line + +- Cut the line on `": "` with `split1` (stack effect + `( seq subseq -- before after )`); discard the part before, keep + the part after. +- Strip whitespace from what's left with `[ blank? ] trim`. + +## 2. Get log level from a log line + +- Splitting on the bracket characters with `"[]" split` gives you + three pieces: `""`, the level, and the rest of the line. +- `harvest` (from [`sequences`][sequences]) drops the empty piece. +- Take the first remaining element and call `>lower` on it. + +## 3. Reformat a log line + +- Reuse `message` and `log-level`. `bi` from `kernel` runs two + quotations on the same input and leaves both results on the stack. +- Wrap the level in parentheses with `surround`. +- Join the message and the parenthesised level with `glue`, using + a single space as the separator. + +[splitting]: https://docs.factorcode.org/content/vocab-splitting.html +[sequences]: https://docs.factorcode.org/content/vocab-sequences.html diff --git a/exercises/concept/log-levels/.docs/instructions.md b/exercises/concept/log-levels/.docs/instructions.md new file mode 100644 index 00000000..7e1c118f --- /dev/null +++ b/exercises/concept/log-levels/.docs/instructions.md @@ -0,0 +1,46 @@ +# Instructions + +In this exercise you'll be processing log lines. + +Each log line is a string formatted as `"[]: "`. + +There are three log levels: `INFO`, `WARNING`, and `ERROR`. + +You have three tasks. Each takes a single log line off the stack. + +## 1. Get message from a log line + +Define `message` to return the log line's message. Any leading or +trailing whitespace should be removed. + +```factor +"[ERROR]: Invalid operation" message . +! => "Invalid operation" + +"[WARNING]: Disk almost full\r\n" message . +! => "Disk almost full" +``` + +## 2. Get log level from a log line + +Define `log-level` to return the log line's level, lowercased. + +```factor +"[ERROR]: Invalid operation" log-level . +! => "error" +``` + +## 3. Reformat a log line + +Define `reformat` to put the message first and the lowercase log level +in parentheses after it. + +```factor +"[INFO]: Operation completed" reformat . +! => "Operation completed (info)" +``` + +---- + +***Note:*** All strings in this exercise are ASCII. Later exercises +will tackle Unicode-aware string handling. diff --git a/exercises/concept/log-levels/.docs/introduction.md b/exercises/concept/log-levels/.docs/introduction.md new file mode 100644 index 00000000..e9061e51 --- /dev/null +++ b/exercises/concept/log-levels/.docs/introduction.md @@ -0,0 +1,95 @@ +# Introduction + +A string in Factor is a sequence of characters. Most words from the +`sequences` vocabulary work on strings, in addition to the dedicated +string-handling words in `splitting`, `ascii`, and friends. + +## String literals + +Strings are written in double quotes. The usual escapes are supported +— `\n`, `\t`, `\r`, `\\`, `\"`: + +```factor +"hello, world" . ! => "hello, world" +"first\nsecond" print ! prints two lines +``` + +## Splitting + +The [`splitting`][splitting] vocabulary cuts a string into pieces. + +`split1` cuts on the *first* occurrence of a separator and returns +both halves: + +``` +split1 ( seq subseq -- before after ) +``` + +```factor +"foo: bar" ": " split1 .s +! => "foo" +! => "bar" +``` + +`split` cuts on *any* of the characters in a separator set, possibly +producing empty pieces: + +```factor +"[ERROR]: Stack overflow" "[]" split . +! => { "" "ERROR" ": Stack overflow" } +``` + +`harvest` (in [`sequences`][sequences]) drops the empty pieces: + +```factor +"[ERROR]: Stack overflow" "[]" split harvest . +! => { "ERROR" ": Stack overflow" } +``` + +## Trimming + +`[ blank? ] trim` (from [`sequences`][sequences], with `blank?` from +the `ascii` vocabulary) removes leading and trailing whitespace: + +```factor +" Disk full \r\n" [ blank? ] trim . +! => "Disk full" +``` + +## Case conversion + +`>lower` and `>upper` (from [`ascii`][ascii]) return a new string +with the case changed: + +```factor +"WARNING" >lower . ! => "warning" +"hello" >upper . ! => "HELLO" +``` + +## Joining + +Two `sequences` words assemble strings: + +`surround` wraps a string with a prefix and a suffix: + +``` +surround ( seq pre post -- new-seq ) +``` + +```factor +"warning" "(" ")" surround . ! => "(warning)" +``` + +`glue` joins two strings with a separator between them: + +``` +glue ( s1 s2 sep -- s ) +``` + +```factor +"Disk full" "(error)" " " glue . ! => "Disk full (error)" +``` + +[splitting]: https://docs.factorcode.org/content/vocab-splitting.html +[sequences]: https://docs.factorcode.org/content/vocab-sequences.html +[ascii]: https://docs.factorcode.org/content/vocab-ascii.html diff --git a/exercises/concept/log-levels/.docs/source.md b/exercises/concept/log-levels/.docs/source.md new file mode 100644 index 00000000..e3a5730a --- /dev/null +++ b/exercises/concept/log-levels/.docs/source.md @@ -0,0 +1,5 @@ +# Source + +Forked from the Julia track's ["Log Levels" exercise][julia-source]. + +[julia-source]: https://github.com/exercism/julia/tree/main/exercises/concept/log-levels diff --git a/exercises/concept/log-levels/.meta/config.json b/exercises/concept/log-levels/.meta/config.json new file mode 100644 index 00000000..044d86ef --- /dev/null +++ b/exercises/concept/log-levels/.meta/config.json @@ -0,0 +1,20 @@ +{ + "authors": [ + "keiravillekode" + ], + "files": { + "solution": [ + "log-levels/log-levels.factor" + ], + "test": [ + "log-levels/log-levels-tests.factor" + ], + "exemplar": [ + ".meta/exemplar.factor" + ] + }, + "forked_from": [ + "julia/log-levels" + ], + "blurb": "Learn about strings in Factor by parsing log lines." +} diff --git a/exercises/concept/log-levels/.meta/design.md b/exercises/concept/log-levels/.meta/design.md new file mode 100644 index 00000000..e716ec4b --- /dev/null +++ b/exercises/concept/log-levels/.meta/design.md @@ -0,0 +1,32 @@ +# Design + +## Goal + +Introduce string handling — splitting, trimming, case conversion, and +joining — by parsing simple `[LEVEL]: message` log lines. + +## Learning objectives + +- Use `split1` and `split` from `splitting` to break a string apart. +- Use `[ blank? ] trim` from `sequences` to strip whitespace. +- Use `>lower` from `ascii` to lowercase a string. +- Use `surround` and `glue` from `sequences` to assemble a result. +- Use `bi` to run two parsing words on the same input. + +## Out of scope + +- Regular expressions. +- Splitting on characters outside ASCII. +- `splitting.fast` and other performance variants. + +## Concepts + +- `strings`: the `splitting`, `ascii`, and `sequences` words used to + pick apart and rebuild a string. + +## Prerequisites + +- `stack-effect` — taught in `lasagna`. +- `conditionals` — taught in `cars-assemble` (the student doesn't + strictly need conditionals here, but the prereq pins this exercise + later in the curriculum, after the easier numeric exercises). diff --git a/exercises/concept/log-levels/.meta/exemplar.factor b/exercises/concept/log-levels/.meta/exemplar.factor new file mode 100644 index 00000000..fe46920f --- /dev/null +++ b/exercises/concept/log-levels/.meta/exemplar.factor @@ -0,0 +1,11 @@ +USING: ascii kernel sequences splitting ; +IN: log-levels + +: message ( log-line -- message ) + ": " split1 nip [ blank? ] trim ; + +: log-level ( log-line -- level ) + "[]" split harvest first >lower ; + +: reformat ( log-line -- formatted ) + [ message ] [ log-level ] bi "(" ")" surround " " glue ; diff --git a/exercises/concept/log-levels/log-levels/log-levels-tests.factor b/exercises/concept/log-levels/log-levels/log-levels-tests.factor new file mode 100644 index 00000000..03592f57 --- /dev/null +++ b/exercises/concept/log-levels/log-levels/log-levels-tests.factor @@ -0,0 +1,18 @@ +USING: log-levels tools.test ; +IN: log-levels.tests + +! TASK: 1 message +{ "Stack overflow" } [ "[ERROR]: Stack overflow" message ] unit-test +{ "Disk almost full" } [ "[WARNING]: Disk almost full" message ] unit-test +{ "File moved" } [ "[INFO]: File moved" message ] unit-test +{ "Timezone not set" } [ "[WARNING]: \tTimezone not set \r\n" message ] unit-test + +! TASK: 2 log-level +{ "error" } [ "[ERROR]: Disk full" log-level ] unit-test +{ "warning" } [ "[WARNING]: Unsafe password" log-level ] unit-test +{ "info" } [ "[INFO]: Timezone changed" log-level ] unit-test + +! TASK: 3 reformat +{ "Decreased performance (warning)" } [ "[WARNING]: Decreased performance" reformat ] unit-test +{ "Disk full (error)" } [ "[ERROR]: Disk full" reformat ] unit-test +{ "File deleted (info)" } [ "[INFO]: File deleted" reformat ] unit-test diff --git a/exercises/concept/log-levels/log-levels/log-levels.factor b/exercises/concept/log-levels/log-levels/log-levels.factor new file mode 100644 index 00000000..e72e1dae --- /dev/null +++ b/exercises/concept/log-levels/log-levels/log-levels.factor @@ -0,0 +1,11 @@ +USING: kernel ; +IN: log-levels + +: message ( log-line -- message ) + "unimplemented" throw ; + +: log-level ( log-line -- level ) + "unimplemented" throw ; + +: reformat ( log-line -- formatted ) + "unimplemented" throw ; From a5eaacba3985e797533ffb35a5cc3b0a85bc14d7 Mon Sep 17 00:00:00 2001 From: Eric Willigers Date: Mon, 27 Apr 2026 00:15:00 +1000 Subject: [PATCH 2/2] currency-conversion --- config.json | 4 ++-- exercises/concept/cars-assemble/.docs/introduction.md | 4 ++-- exercises/concept/cars-assemble/.meta/design.md | 2 +- .../{currency-exchange => currency-conversion}/.docs/hints.md | 0 .../.docs/instructions.md | 0 .../.docs/introduction.md | 0 .../.docs/source.md | 0 .../.meta/config.json | 4 ++-- .../.meta/design.md | 0 .../.meta/exemplar.factor | 2 +- .../currency-conversion/currency-conversion-tests.factor} | 4 ++-- .../currency-conversion/currency-conversion.factor} | 2 +- 12 files changed, 11 insertions(+), 11 deletions(-) rename exercises/concept/{currency-exchange => currency-conversion}/.docs/hints.md (100%) rename exercises/concept/{currency-exchange => currency-conversion}/.docs/instructions.md (100%) rename exercises/concept/{currency-exchange => currency-conversion}/.docs/introduction.md (100%) rename exercises/concept/{currency-exchange => currency-conversion}/.docs/source.md (100%) rename exercises/concept/{currency-exchange => currency-conversion}/.meta/config.json (72%) rename exercises/concept/{currency-exchange => currency-conversion}/.meta/design.md (100%) rename exercises/concept/{currency-exchange => currency-conversion}/.meta/exemplar.factor (96%) rename exercises/concept/{currency-exchange/currency-exchange/currency-exchange-tests.factor => currency-conversion/currency-conversion/currency-conversion-tests.factor} (94%) rename exercises/concept/{currency-exchange/currency-exchange/currency-exchange.factor => currency-conversion/currency-conversion/currency-conversion.factor} (95%) diff --git a/config.json b/config.json index 414cf5f4..2d3ed5a1 100644 --- a/config.json +++ b/config.json @@ -54,8 +54,8 @@ "status": "wip" }, { - "slug": "currency-exchange", - "name": "Currency Exchange", + "slug": "currency-conversion", + "name": "Currency Conversion", "uuid": "380a2d4f-ea48-47a1-94f1-fc8ec0561136", "concepts": [ "numbers" diff --git a/exercises/concept/cars-assemble/.docs/introduction.md b/exercises/concept/cars-assemble/.docs/introduction.md index 4fe46e62..3e6573e4 100644 --- a/exercises/concept/cars-assemble/.docs/introduction.md +++ b/exercises/concept/cars-assemble/.docs/introduction.md @@ -3,7 +3,7 @@ This exercise introduces conditionals — choosing between two or more courses of action based on a value. It builds on the booleans you met in [Annalyn's Infiltration][annalyns] and the integer arithmetic from -[Currency Exchange][currency-exchange]. +[Currency Conversion][currency-conversion]. ## Comparison words @@ -75,7 +75,7 @@ A few details worth noting: result. [annalyns]: https://exercism.org/tracks/factor/exercises/annalyns-infiltration -[currency-exchange]: https://exercism.org/tracks/factor/exercises/currency-exchange +[currency-conversion]: https://exercism.org/tracks/factor/exercises/currency-conversion [math]: https://docs.factorcode.org/content/vocab-math.html [kernel]: https://docs.factorcode.org/content/vocab-kernel.html [combinators]: https://docs.factorcode.org/content/vocab-combinators.html diff --git a/exercises/concept/cars-assemble/.meta/design.md b/exercises/concept/cars-assemble/.meta/design.md index 8ab404aa..b843f08e 100644 --- a/exercises/concept/cars-assemble/.meta/design.md +++ b/exercises/concept/cars-assemble/.meta/design.md @@ -28,4 +28,4 @@ Introduce Factor's conditional combinators by mapping a discrete input ## Prerequisites - `booleans` — taught in `annalyns-infiltration`. -- `numbers` — taught in `currency-exchange`. +- `numbers` — taught in `currency-conversion`. diff --git a/exercises/concept/currency-exchange/.docs/hints.md b/exercises/concept/currency-conversion/.docs/hints.md similarity index 100% rename from exercises/concept/currency-exchange/.docs/hints.md rename to exercises/concept/currency-conversion/.docs/hints.md diff --git a/exercises/concept/currency-exchange/.docs/instructions.md b/exercises/concept/currency-conversion/.docs/instructions.md similarity index 100% rename from exercises/concept/currency-exchange/.docs/instructions.md rename to exercises/concept/currency-conversion/.docs/instructions.md diff --git a/exercises/concept/currency-exchange/.docs/introduction.md b/exercises/concept/currency-conversion/.docs/introduction.md similarity index 100% rename from exercises/concept/currency-exchange/.docs/introduction.md rename to exercises/concept/currency-conversion/.docs/introduction.md diff --git a/exercises/concept/currency-exchange/.docs/source.md b/exercises/concept/currency-conversion/.docs/source.md similarity index 100% rename from exercises/concept/currency-exchange/.docs/source.md rename to exercises/concept/currency-conversion/.docs/source.md diff --git a/exercises/concept/currency-exchange/.meta/config.json b/exercises/concept/currency-conversion/.meta/config.json similarity index 72% rename from exercises/concept/currency-exchange/.meta/config.json rename to exercises/concept/currency-conversion/.meta/config.json index 893ad928..122f6a91 100644 --- a/exercises/concept/currency-exchange/.meta/config.json +++ b/exercises/concept/currency-conversion/.meta/config.json @@ -4,10 +4,10 @@ ], "files": { "solution": [ - "currency-exchange/currency-exchange.factor" + "currency-conversion/currency-conversion.factor" ], "test": [ - "currency-exchange/currency-exchange-tests.factor" + "currency-conversion/currency-conversion-tests.factor" ], "exemplar": [ ".meta/exemplar.factor" diff --git a/exercises/concept/currency-exchange/.meta/design.md b/exercises/concept/currency-conversion/.meta/design.md similarity index 100% rename from exercises/concept/currency-exchange/.meta/design.md rename to exercises/concept/currency-conversion/.meta/design.md diff --git a/exercises/concept/currency-exchange/.meta/exemplar.factor b/exercises/concept/currency-conversion/.meta/exemplar.factor similarity index 96% rename from exercises/concept/currency-exchange/.meta/exemplar.factor rename to exercises/concept/currency-conversion/.meta/exemplar.factor index eebbec0c..9e4d96a9 100644 --- a/exercises/concept/currency-exchange/.meta/exemplar.factor +++ b/exercises/concept/currency-conversion/.meta/exemplar.factor @@ -1,5 +1,5 @@ USING: kernel locals math math.functions ; -IN: currency-exchange +IN: currency-conversion : exchange-money ( budget exchange-rate -- exchanged ) / ; diff --git a/exercises/concept/currency-exchange/currency-exchange/currency-exchange-tests.factor b/exercises/concept/currency-conversion/currency-conversion/currency-conversion-tests.factor similarity index 94% rename from exercises/concept/currency-exchange/currency-exchange/currency-exchange-tests.factor rename to exercises/concept/currency-conversion/currency-conversion/currency-conversion-tests.factor index 138d7907..543622ca 100644 --- a/exercises/concept/currency-exchange/currency-exchange/currency-exchange-tests.factor +++ b/exercises/concept/currency-conversion/currency-conversion/currency-conversion-tests.factor @@ -1,5 +1,5 @@ -USING: currency-exchange tools.test ; -IN: currency-exchange.tests +USING: currency-conversion tools.test ; +IN: currency-conversion.tests ! TASK: 1 exchange-money { 125000.0 } [ 100000 0.8 exchange-money ] unit-test diff --git a/exercises/concept/currency-exchange/currency-exchange/currency-exchange.factor b/exercises/concept/currency-conversion/currency-conversion/currency-conversion.factor similarity index 95% rename from exercises/concept/currency-exchange/currency-exchange/currency-exchange.factor rename to exercises/concept/currency-conversion/currency-conversion/currency-conversion.factor index ab016859..9290750c 100644 --- a/exercises/concept/currency-exchange/currency-exchange/currency-exchange.factor +++ b/exercises/concept/currency-conversion/currency-conversion/currency-conversion.factor @@ -1,5 +1,5 @@ USING: kernel ; -IN: currency-exchange +IN: currency-conversion : exchange-money ( budget exchange-rate -- exchanged ) "unimplemented" throw ;