diff --git a/config.json b/config.json index fa215ef..801f893 100644 --- a/config.json +++ b/config.json @@ -426,6 +426,14 @@ "prerequisites": [], "difficulty": 4 }, + { + "slug": "resistor-color", + "name": "Resistor Color", + "uuid": "189eb2e7-d2b2-4439-aa13-ade013566bdb", + "practices": [], + "prerequisites": [], + "difficulty": 4 + }, { "slug": "roman-numerals", "name": "Roman Numerals", diff --git a/exercises/practice/resistor-color/.docs/instructions.append.md b/exercises/practice/resistor-color/.docs/instructions.append.md new file mode 100644 index 0000000..3eea3b8 --- /dev/null +++ b/exercises/practice/resistor-color/.docs/instructions.append.md @@ -0,0 +1,19 @@ +# Instructions append + +## Defining syntax + +In this exercise, you must define syntax for colors using the `c!` prefix, e.g., `c!black`. +This syntax should expand at compile time to each color's corresponding value as a `Fin 10`, according to the instructions. +In the same way, you must define syntax for an array of all color values using `c!all`. + +This task will likely require you to use either notations or macros. +You might want to check a [reference][macro-reference]. + +Because new syntax is expanded at compile time, any test would fail to compile unless all the required syntax is defined. +For this reason, instead of relying on traditional runtime tests, we check all values using theorems. +If a theorem typechecks, you may consider it a passing test. + +If you work locally or in Lean's [online playground][playground], you will get instant feedback on whether any theorem succeeds or fails, and why it fails, through Lean InfoView. + +[macro-reference]: https://lean-lang.org/doc/reference/latest/Notations-and-Macros/#language-extension +[playground]: https://live.lean-lang.org/ diff --git a/exercises/practice/resistor-color/.docs/instructions.md b/exercises/practice/resistor-color/.docs/instructions.md new file mode 100644 index 0000000..0125e71 --- /dev/null +++ b/exercises/practice/resistor-color/.docs/instructions.md @@ -0,0 +1,39 @@ +# Instructions + +If you want to build something using a Raspberry Pi, you'll probably use _resistors_. +For this exercise, you need to know two things about them: + +- Each resistor has a resistance value. +- Resistors are small - so small in fact that if you printed the resistance value on them, it would be hard to read. + +To get around this problem, manufacturers print color-coded bands onto the resistors to denote their resistance values. +Each band has a position and a numeric value. + +The first 2 bands of a resistor have a simple encoding scheme: each color maps to a single number. + +In this exercise you are going to create a helpful program so that you don't have to remember the values of the bands. + +These colors are encoded as follows: + +- black: 0 +- brown: 1 +- red: 2 +- orange: 3 +- yellow: 4 +- green: 5 +- blue: 6 +- violet: 7 +- grey: 8 +- white: 9 + +The goal of this exercise is to create a way: + +- to look up the numerical value associated with a particular color band +- to list the different band colors + +Mnemonics map the colors to the numbers, that, when stored as an array, happen to map to their index in the array: +Better Be Right Or Your Great Big Values Go Wrong. + +More information on the color encoding of resistors can be found in the [Electronic color code Wikipedia article][e-color-code]. + +[e-color-code]: https://en.wikipedia.org/wiki/Electronic_color_code diff --git a/exercises/practice/resistor-color/.meta/Example.lean b/exercises/practice/resistor-color/.meta/Example.lean new file mode 100644 index 0000000..69f2307 --- /dev/null +++ b/exercises/practice/resistor-color/.meta/Example.lean @@ -0,0 +1,28 @@ +/- +notation "c!black" => (0 : Fin 10) +notation "c!brown" => (1 : Fin 10) +notation "c!red" => (2 : Fin 10) +notation "c!orange" => (3 : Fin 10) +notation "c!yellow" => (4 : Fin 10) +notation "c!green" => (5 : Fin 10) +notation "c!blue" => (6 : Fin 10) +notation "c!violet" => (7 : Fin 10) +notation "c!grey" => (8 : Fin 10) +notation "c!white" => (9 : Fin 10) +notation "c!all" => (Array.finRange 10) +-/ + +syntax "c!" ident:term + +macro_rules + | `(c!black) => `((⟨0, by decide⟩ : Fin 10)) + | `(c!brown) => `((⟨1, by decide⟩ : Fin 10)) + | `(c!red) => `((⟨2, by decide⟩ : Fin 10)) + | `(c!orange) => `((⟨3, by decide⟩ : Fin 10)) + | `(c!yellow) => `((⟨4, by decide⟩ : Fin 10)) + | `(c!green) => `((⟨5, by decide⟩ : Fin 10)) + | `(c!blue) => `((⟨6, by decide⟩ : Fin 10)) + | `(c!violet) => `((⟨7, by decide⟩ : Fin 10)) + | `(c!grey) => `((⟨8, by decide⟩ : Fin 10)) + | `(c!white) => `((⟨9, by decide⟩ : Fin 10)) + | `(c!all) => `((Array.finRange 10)) diff --git a/exercises/practice/resistor-color/.meta/config.json b/exercises/practice/resistor-color/.meta/config.json new file mode 100644 index 0000000..e6e8ab8 --- /dev/null +++ b/exercises/practice/resistor-color/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "oxe-i" + ], + "files": { + "solution": [ + "ResistorColor.lean" + ], + "test": [ + "ResistorColorTest.lean" + ], + "example": [ + ".meta/Example.lean" + ] + }, + "blurb": "Convert a resistor band's color to its numeric representation.", + "source": "Maud de Vries, Erik Schierboom", + "source_url": "https://github.com/exercism/problem-specifications/issues/1458" +} diff --git a/exercises/practice/resistor-color/.meta/tests.toml b/exercises/practice/resistor-color/.meta/tests.toml new file mode 100644 index 0000000..9d4ee97 --- /dev/null +++ b/exercises/practice/resistor-color/.meta/tests.toml @@ -0,0 +1,22 @@ +# 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. + +[49eb31c5-10a8-4180-9f7f-fea632ab87ef] +description = "Color codes -> Black" + +[0a4df94b-92da-4579-a907-65040ce0b3fc] +description = "Color codes -> White" + +[5f81608d-f36f-4190-8084-f45116b6f380] +description = "Color codes -> Orange" + +[581d68fa-f968-4be2-9f9d-880f2fb73cf7] +description = "Colors" diff --git a/exercises/practice/resistor-color/ResistorColor.lean b/exercises/practice/resistor-color/ResistorColor.lean new file mode 100644 index 0000000..82e59bb --- /dev/null +++ b/exercises/practice/resistor-color/ResistorColor.lean @@ -0,0 +1,9 @@ +/- + You must define syntax for colors using the `c!` prefix, e.g., `c!black`. + This syntax should expand to its corresponding value as a `Fin 10`, according to the instructions. + + You must also define syntax `c!all` that expands to an array containing all color values. + + Macros and notations are not qualified by namespace. + For this reason, this exercise does not define a namespace. +-/ diff --git a/exercises/practice/resistor-color/ResistorColorTest.lean b/exercises/practice/resistor-color/ResistorColorTest.lean new file mode 100644 index 0000000..813be64 --- /dev/null +++ b/exercises/practice/resistor-color/ResistorColorTest.lean @@ -0,0 +1,19 @@ +import LeanTest +import ResistorColor + +open LeanTest + +theorem h_black: c!black = (0 : Fin 10) := by rfl +theorem h_brown: c!brown = (1 : Fin 10) := by rfl +theorem h_red: c!red = (2 : Fin 10) := by rfl +theorem h_orange: c!orange = (3 : Fin 10) := by rfl +theorem h_yellow: c!yellow = (4 : Fin 10) := by rfl +theorem h_green: c!green = (5 : Fin 10) := by rfl +theorem h_blue: c!blue = (6 : Fin 10) := by rfl +theorem h_violet: c!violet = (7 : Fin 10) := by rfl +theorem h_grey: c!grey = (8 : Fin 10) := by rfl +theorem h_white: c!white = (9 : Fin 10) := by rfl +theorem h_all: c!all = #[c!black, c!brown, c!red, c!orange, c!yellow, c!green, c!blue, c!violet, c!grey, c!white] := by rfl + +def main : IO UInt32 := do + runTestSuitesWithExitCode [] diff --git a/exercises/practice/resistor-color/lakefile.toml b/exercises/practice/resistor-color/lakefile.toml new file mode 100644 index 0000000..edfab95 --- /dev/null +++ b/exercises/practice/resistor-color/lakefile.toml @@ -0,0 +1,15 @@ +name = "resistor-color" +version = "0.1.0" +defaultTargets = ["ResistorColorTest"] +testDriver = "ResistorColorTest" + +[[lean_lib]] +name = "LeanTest" +srcDir = "vendor/LeanTest" + +[[lean_lib]] +name = "ResistorColor" + +[[lean_exe]] +name = "ResistorColorTest" +root = "ResistorColorTest" diff --git a/exercises/practice/resistor-color/lean-toolchain b/exercises/practice/resistor-color/lean-toolchain new file mode 100644 index 0000000..14791d7 --- /dev/null +++ b/exercises/practice/resistor-color/lean-toolchain @@ -0,0 +1 @@ +leanprover/lean4:v4.29.0 diff --git a/exercises/practice/resistor-color/vendor/LeanTest/LeanTest.lean b/exercises/practice/resistor-color/vendor/LeanTest/LeanTest.lean new file mode 100644 index 0000000..012ba20 --- /dev/null +++ b/exercises/practice/resistor-color/vendor/LeanTest/LeanTest.lean @@ -0,0 +1,4 @@ +-- This module serves as the root of the `LeanTest` library. +-- Import modules here that should be built as part of the library. +import LeanTest.Assertions +import LeanTest.Test diff --git a/exercises/practice/resistor-color/vendor/LeanTest/LeanTest/Assertions.lean b/exercises/practice/resistor-color/vendor/LeanTest/LeanTest/Assertions.lean new file mode 100644 index 0000000..60e4ad8 --- /dev/null +++ b/exercises/practice/resistor-color/vendor/LeanTest/LeanTest/Assertions.lean @@ -0,0 +1,166 @@ +/- +Assertion functions for unit testing. +-/ + +namespace LeanTest + +/-- Result of a test assertion -/ +inductive AssertionResult where + | success : AssertionResult + | failure (message : String) : AssertionResult + deriving Repr, BEq + +namespace AssertionResult + +def isSuccess : AssertionResult → Bool + | success => true + | failure _ => false + +def getMessage : AssertionResult → String + | success => "Assertion passed" + | failure msg => msg + +end AssertionResult + +/-- Assert that a boolean condition is true -/ +def assert (condition : Bool) (message : String := "Assertion failed") : AssertionResult := + if condition then + .success + else + .failure message + +/-- Assert that two values are equal -/ +def assertEqual [BEq α] [Repr α] (expected : α) (actual : α) (message : String := "") : AssertionResult := + if expected == actual then + .success + else + let msg := if message.isEmpty then + s!"Expected: {repr expected}\nActual: {repr actual}" + else + s!"{message}\nExpected: {repr expected}\nActual: {repr actual}" + .failure msg + +/-- Assert that two values are not equal -/ +def assertNotEqual [BEq α] [Repr α] (expected : α) (actual : α) (message : String := "") : AssertionResult := + if expected != actual then + .success + else + let msg := if message.isEmpty then + s!"Expected values to be different, but both were: {repr expected}" + else + s!"{message}\nExpected values to be different, but both were: {repr expected}" + .failure msg + +/-- Refute that a boolean condition is true (assert it's false) -/ +def refute (condition : Bool) (message : String := "Refute failed - condition was true") : AssertionResult := + if !condition then + .success + else + .failure message + +/-- Assert that a value is true -/ +def assertTrue (value : Bool) (message : String := "Expected true but got false") : AssertionResult := + assert value message + +/-- Assert that a value is false -/ +def assertFalse (value : Bool) (message : String := "Expected false but got true") : AssertionResult := + refute value message + +/-- Assert that an Option is some -/ +def assertSome [Repr α] (opt : Option α) (message : String := "Expected Some but got None") : AssertionResult := + match opt with + | some _ => .success + | none => .failure message + +/-- Assert that an Option is none -/ +def assertNone [Repr α] (opt : Option α) (message : String := "") : AssertionResult := + match opt with + | none => .success + | some val => + let msg := if message.isEmpty then + s!"Expected None but got Some: {repr val}" + else + s!"{message}\nExpected None but got Some: {repr val}" + .failure msg + +/-- Assert that a list is empty -/ +def assertEmpty [Repr α] (list : List α) (message : String := "") : AssertionResult := + match list with + | [] => .success + | _ => + let msg := if message.isEmpty then + s!"Expected empty list but got: {repr list}" + else + s!"{message}\nExpected empty list but got: {repr list}" + .failure msg + +/-- Assert that a list contains an element -/ +def assertContains [BEq α] [Repr α] (list : List α) (element : α) (message : String := "") : AssertionResult := + if list.contains element then + .success + else + let msg := if message.isEmpty then + s!"Expected list to contain {repr element}\nList: {repr list}" + else + s!"{message}\nExpected list to contain {repr element}\nList: {repr list}" + .failure msg + +/-- Assert that a value is within a range (inclusive) -/ +def assertInRange [LE α] [DecidableRel (· ≤ · : α → α → Prop)] [Repr α] + (value : α) (min : α) (max : α) (message : String := "") : AssertionResult := + if min ≤ value ∧ value ≤ max then + .success + else + let msg := if message.isEmpty then + s!"Expected {repr value} to be in range [{repr min}, {repr max}]" + else + s!"{message}\nExpected {repr value} to be in range [{repr min}, {repr max}]" + .failure msg + +/-- Assert that an Except value is an error -/ +def assertError [Repr ε] [Repr α] (result : Except ε α) (message : String := "") : AssertionResult := + match result with + | .error _ => .success + | .ok val => + let msg := if message.isEmpty then + s!"Expected error but got Ok: {repr val}" + else + s!"{message}\nExpected error but got Ok: {repr val}" + .failure msg + +/-- Assert that an Except value is ok -/ +def assertOk [Repr ε] [Repr α] (result : Except ε α) (message : String := "") : AssertionResult := + match result with + | .ok _ => .success + | .error err => + let msg := if message.isEmpty then + s!"Expected Ok but got error: {repr err}" + else + s!"{message}\nExpected Ok but got error: {repr err}" + .failure msg + +/-- Assert that an IO action throws an error -/ +def assertThrows (action : IO α) (message : String := "") : IO AssertionResult := do + try + let _ ← action + let msg := if message.isEmpty then + "Expected IO action to throw an error, but it succeeded" + else + s!"{message}\nExpected IO action to throw an error, but it succeeded" + return .failure msg + catch _ => + return .success + +/-- Assert that an IO action succeeds (doesn't throw) -/ +def assertSucceeds (action : IO α) (message : String := "") : IO AssertionResult := do + try + let _ ← action + return .success + catch e => + let msg := if message.isEmpty then + s!"Expected IO action to succeed, but it threw: {e}" + else + s!"{message}\nExpected IO action to succeed, but it threw: {e}" + return .failure msg + +end LeanTest diff --git a/exercises/practice/resistor-color/vendor/LeanTest/LeanTest/Test.lean b/exercises/practice/resistor-color/vendor/LeanTest/LeanTest/Test.lean new file mode 100644 index 0000000..5ddbae5 --- /dev/null +++ b/exercises/practice/resistor-color/vendor/LeanTest/LeanTest/Test.lean @@ -0,0 +1,130 @@ +/- +Test case and test suite management. +-/ + +import LeanTest.Assertions + +namespace LeanTest + +/-- A single test case -/ +structure TestCase where + description : String + test : IO AssertionResult + deriving Inhabited + +/-- Result of running a test -/ +structure TestResult where + description : String + result : AssertionResult + deriving Repr + +/-- A collection of tests (test suite) -/ +structure TestSuite where + name : String + tests : List TestCase + deriving Inhabited + +namespace TestSuite + +/-- Create an empty test suite -/ +def empty (name : String) : TestSuite := + { name := name, tests := [] } + +/-- Add a test to the suite -/ +def addTest (suite : TestSuite) (description : String) (test : IO AssertionResult) : TestSuite := + { suite with tests := suite.tests ++ [{ description := description, test := test }] } + +end TestSuite + +/-- Test statistics -/ +structure TestStats where + total : Nat + passed : Nat + failed : Nat + deriving Repr + +namespace TestStats + +def empty : TestStats := + { total := 0, passed := 0, failed := 0 } + +def addResult (stats : TestStats) (result : AssertionResult) : TestStats := + { total := stats.total + 1 + , passed := if result.isSuccess then stats.passed + 1 else stats.passed + , failed := if result.isSuccess then stats.failed else stats.failed + 1 + } + +end TestStats + +/-- ANSI color codes for terminal output -/ +def greenColor : String := "\x1b[32m" +def redColor : String := "\x1b[31m" +def yellowColor : String := "\x1b[33m" +def resetColor : String := "\x1b[0m" +def boldColor : String := "\x1b[1m" + +/-- Run a single test and print the result -/ +def runTest (testCase : TestCase) : IO TestResult := do + let result ← testCase.test + match result with + | .success => + IO.println s!" {greenColor}✓{resetColor} {testCase.description}" + | .failure msg => + IO.println s!" {redColor}✗{resetColor} {testCase.description}" + IO.println s!" {redColor}{msg}{resetColor}" + return { description := testCase.description, result := result } + +/-- Run all tests in a test suite -/ +def runTestSuite (suite : TestSuite) : IO TestStats := do + IO.println s!"\n{boldColor}{suite.name}{resetColor}" + let mut stats := TestStats.empty + + for testCase in suite.tests do + let result ← runTest testCase + stats := stats.addResult result.result + + return stats + +/-- Print test summary -/ +def printSummary (stats : TestStats) : IO Unit := do + IO.println "" + IO.println s!"{boldColor}Test Summary:{resetColor}" + IO.println s!" Total: {stats.total}" + IO.println s!" {greenColor}Passed: {stats.passed}{resetColor}" + + if stats.failed > 0 then + IO.println s!" {redColor}Failed: {stats.failed}{resetColor}" + IO.println s!"\n{redColor}FAILED{resetColor}" + else + IO.println s!"\n{greenColor}ALL TESTS PASSED{resetColor}" + +/-- Run multiple test suites -/ +def runTestSuites (suites : List TestSuite) : IO Unit := do + let mut totalStats := TestStats.empty + + for suite in suites do + let stats ← runTestSuite suite + totalStats := { + total := totalStats.total + stats.total, + passed := totalStats.passed + stats.passed, + failed := totalStats.failed + stats.failed + } + + printSummary totalStats + +/-- Run multiple test suites and return exit code (0 = all passed, 1 = some failed) -/ +def runTestSuitesWithExitCode (suites : List TestSuite) : IO UInt32 := do + let mut totalStats := TestStats.empty + + for suite in suites do + let stats ← runTestSuite suite + totalStats := { + total := totalStats.total + stats.total, + passed := totalStats.passed + stats.passed, + failed := totalStats.failed + stats.failed + } + + printSummary totalStats + return if totalStats.failed > 0 then 1 else 0 + +end LeanTest diff --git a/generators/Generator/Generator.lean b/generators/Generator/Generator.lean index 3d0400d..b3023e2 100644 --- a/generators/Generator/Generator.lean +++ b/generators/Generator/Generator.lean @@ -1,3 +1,4 @@ +import Generator.ResistorColorGenerator import Generator.StrainGenerator import Generator.NucleotideCountGenerator import Generator.HangmanGenerator @@ -101,6 +102,7 @@ abbrev endBodyGenerator := String -> String def dispatch : Std.HashMap String (introGenerator × testCaseGenerator × endBodyGenerator) := Std.HashMap.ofList [ + ("ResistorColor", (ResistorColorGenerator.genIntro, ResistorColorGenerator.genTestCase, ResistorColorGenerator.genEnd)), ("Strain", (StrainGenerator.genIntro, StrainGenerator.genTestCase, StrainGenerator.genEnd)), ("NucleotideCount", (NucleotideCountGenerator.genIntro, NucleotideCountGenerator.genTestCase, NucleotideCountGenerator.genEnd)), ("Hangman", (HangmanGenerator.genIntro, HangmanGenerator.genTestCase, HangmanGenerator.genEnd)), diff --git a/generators/Generator/Generator/ResistorColorGenerator.lean b/generators/Generator/Generator/ResistorColorGenerator.lean new file mode 100644 index 0000000..4c96587 --- /dev/null +++ b/generators/Generator/Generator/ResistorColorGenerator.lean @@ -0,0 +1,41 @@ +import Lean.Data.Json +import Std +import Helper + +open Lean +open Std +open Helper + +namespace ResistorColorGenerator + +def genIntro (exercise : String) : String := s!"import LeanTest +import {exercise} + +open LeanTest +" + +def genTestCase (_exercise : String) (case : TreeMap.Raw String Json) : String := + let _ := case.get! "input" + let expected := case.get! "expected" + let _ := case.get! "description" + |> (·.compress) + let funName := getFunName (case.get! "property") + match funName with + | "colors" => + let result := expected.getArr?.map (·.map (·.compress |> toLiteral |> (s!"c!{·}"))) |> getOk + let theorems := result.toList.mapIdx fun i color => + let c := color.dropPrefix "c!" |>.copy + s!" +theorem h_{c}: {color} = ({i} : Fin 10) := by rfl" + (String.join theorems) ++ s!" +theorem h_all: c!all = {result} := by rfl" + | _ => "" + +def genEnd (_exercise : String) : String := + s!" + +def main : IO UInt32 := do + runTestSuitesWithExitCode [] +" + +end ResistorColorGenerator