From d52ec7c4882297ca9c835f0757ab130f8f71182d Mon Sep 17 00:00:00 2001 From: tofische Date: Wed, 17 Jan 2024 22:39:41 +0100 Subject: [PATCH 1/3] Added Rational Numbers exercise --- config.json | 13 + .../rational-numbers/.docs/instructions.md | 42 +++ .../rational-numbers/.meta/config.json | 23 ++ .../examples/success-standard/package.yaml | 16 + .../success-standard/src/RationalNumbers.hs | 50 +++ .../rational-numbers/.meta/tests.toml | 139 ++++++++ .../practice/rational-numbers/package.yaml | 21 ++ .../rational-numbers/src/RationalNumbers.hs | 46 +++ .../practice/rational-numbers/stack.yaml | 1 + .../practice/rational-numbers/test/Tests.hs | 305 ++++++++++++++++++ 10 files changed, 656 insertions(+) create mode 100644 exercises/practice/rational-numbers/.docs/instructions.md create mode 100644 exercises/practice/rational-numbers/.meta/config.json create mode 100644 exercises/practice/rational-numbers/.meta/examples/success-standard/package.yaml create mode 100644 exercises/practice/rational-numbers/.meta/examples/success-standard/src/RationalNumbers.hs create mode 100644 exercises/practice/rational-numbers/.meta/tests.toml create mode 100644 exercises/practice/rational-numbers/package.yaml create mode 100644 exercises/practice/rational-numbers/src/RationalNumbers.hs create mode 100644 exercises/practice/rational-numbers/stack.yaml create mode 100644 exercises/practice/rational-numbers/test/Tests.hs diff --git a/config.json b/config.json index 413e79bee..4d2b7571c 100644 --- a/config.json +++ b/config.json @@ -1109,6 +1109,19 @@ "number_theory" ] }, + { + "slug": "rational-numbers", + "name": "Rational Numbers", + "uuid": "b4426fe9-2e49-48c5-9dea-c86032009687", + "practices": [], + "prerequisites": [], + "difficulty": 3, + "topics": [ + "define_type", + "math", + "number_theory" + ] + }, { "slug": "largest-series-product", "name": "Largest Series Product", diff --git a/exercises/practice/rational-numbers/.docs/instructions.md b/exercises/practice/rational-numbers/.docs/instructions.md new file mode 100644 index 000000000..f64fc0f28 --- /dev/null +++ b/exercises/practice/rational-numbers/.docs/instructions.md @@ -0,0 +1,42 @@ +# Instructions + +A rational number is defined as the quotient of two integers `a` and `b`, called the numerator and denominator, respectively, where `b != 0`. + +~~~~exercism/note +Note that mathematically, the denominator can't be zero. +However in many implementations of rational numbers, you will find that the denominator is allowed to be zero with behaviour similar to positive or negative infinity in floating point numbers. +In those cases, the denominator and numerator generally still can't both be zero at once. +~~~~ + +The absolute value `|r|` of the rational number `r = a/b` is equal to `|a|/|b|`. + +The sum of two rational numbers `r₁ = a₁/b₁` and `r₂ = a₂/b₂` is `r₁ + r₂ = a₁/b₁ + a₂/b₂ = (a₁ * b₂ + a₂ * b₁) / (b₁ * b₂)`. + +The difference of two rational numbers `r₁ = a₁/b₁` and `r₂ = a₂/b₂` is `r₁ - r₂ = a₁/b₁ - a₂/b₂ = (a₁ * b₂ - a₂ * b₁) / (b₁ * b₂)`. + +The product (multiplication) of two rational numbers `r₁ = a₁/b₁` and `r₂ = a₂/b₂` is `r₁ * r₂ = (a₁ * a₂) / (b₁ * b₂)`. + +Dividing a rational number `r₁ = a₁/b₁` by another `r₂ = a₂/b₂` is `r₁ / r₂ = (a₁ * b₂) / (a₂ * b₁)` if `a₂` is not zero. + +Exponentiation of a rational number `r = a/b` to a non-negative integer power `n` is `r^n = (a^n)/(b^n)`. + +Exponentiation of a rational number `r = a/b` to a negative integer power `n` is `r^n = (b^m)/(a^m)`, where `m = |n|`. + +Exponentiation of a rational number `r = a/b` to a real (floating-point) number `x` is the quotient `(a^x)/(b^x)`, which is a real number. + +Exponentiation of a real number `x` to a rational number `r = a/b` is `x^(a/b) = root(x^a, b)`, where `root(p, q)` is the `q`th root of `p`. + +Implement the following operations: + +- addition, subtraction, multiplication and division of two rational numbers, +- absolute value, exponentiation of a given rational number to an integer power, exponentiation of a given rational number to a real (floating-point) power, exponentiation of a real number to a rational number. + +Your implementation of rational numbers should always be reduced to lowest terms. +For example, `4/4` should reduce to `1/1`, `30/60` should reduce to `1/2`, `12/8` should reduce to `3/2`, etc. +To reduce a rational number `r = a/b`, divide `a` and `b` by the greatest common divisor (gcd) of `a` and `b`. +So, for example, `gcd(12, 8) = 4`, so `r = 12/8` can be reduced to `(12/4)/(8/4) = 3/2`. +The reduced form of a rational number should be in "standard form" (the denominator should always be a positive integer). +If a denominator with a negative integer is present, multiply both numerator and denominator by `-1` to ensure standard form is reached. +For example, `3/-4` should be reduced to `-3/4` + +Assume that the programming language you are using does not have an implementation of rational numbers. diff --git a/exercises/practice/rational-numbers/.meta/config.json b/exercises/practice/rational-numbers/.meta/config.json new file mode 100644 index 000000000..5c02fd8f9 --- /dev/null +++ b/exercises/practice/rational-numbers/.meta/config.json @@ -0,0 +1,23 @@ +{ + "authors": [ + "tofische" + ], + "files": { + "solution": [ + "src/RationalNumbers.hs", + "package.yaml" + ], + "test": [ + "test/Tests.hs" + ], + "example": [ + ".meta/examples/success-standard/src/RationalNumbers.hs" + ], + "invalidator": [ + "stack.yaml" + ] + }, + "blurb": "Implement rational numbers.", + "source": "Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/Rational_number" +} diff --git a/exercises/practice/rational-numbers/.meta/examples/success-standard/package.yaml b/exercises/practice/rational-numbers/.meta/examples/success-standard/package.yaml new file mode 100644 index 000000000..e55cce9f0 --- /dev/null +++ b/exercises/practice/rational-numbers/.meta/examples/success-standard/package.yaml @@ -0,0 +1,16 @@ +name: rational-numbers + +dependencies: + - base + +library: + exposed-modules: RationalNumbers + source-dirs: src + +tests: + test: + main: Tests.hs + source-dirs: test + dependencies: + - rational-numbers + - hspec diff --git a/exercises/practice/rational-numbers/.meta/examples/success-standard/src/RationalNumbers.hs b/exercises/practice/rational-numbers/.meta/examples/success-standard/src/RationalNumbers.hs new file mode 100644 index 000000000..b98b185d0 --- /dev/null +++ b/exercises/practice/rational-numbers/.meta/examples/success-standard/src/RationalNumbers.hs @@ -0,0 +1,50 @@ +module RationalNumbers +(Rational, + abs, + reduce, + add, + sub, + mul, + div, + exprational, + expreal, + rational) where + +import Prelude hiding (div, abs, Rational) +import qualified Prelude as P + +-- Data definition ------------------------------------------------------------- +data Rational a = Rational a a deriving(Eq, Show) + +rational :: (a, a) -> Rational a +rational (n, d) = Rational n d + +-- unary operators ------------------------------------------------------------- +abs :: Integral a => Rational a -> Rational a +abs (Rational n d ) = reduce $ Rational (P.abs n) (P.abs d) + +reduce :: Integral a => Rational a -> Rational a +reduce (Rational n d) = Rational (n' `quot` g) (d' `quot` g) + where + g = gcd n d + (n', d') = if d < 0 then (-n, -d) else (n, d) + + +-- binary operators ------------------------------------------------------------ +add :: Integral a => Rational a -> Rational a -> Rational a +add (Rational n1 d1) (Rational n2 d2) = let dd = d1*d2 in reduce $ Rational (n1*d2 + n2*d1) dd + +sub :: Integral a => Rational a -> Rational a -> Rational a +sub (Rational n1 d1) (Rational n2 d2) = let dd = d1*d2 in reduce $ Rational (n1*d2 - n2*d1) dd + +mul :: Integral a => Rational a -> Rational a -> Rational a +mul (Rational n1 d1) (Rational n2 d2) = reduce $ Rational (n1*n2) (d1*d2) + +div :: Integral a => Rational a -> Rational a -> Rational a +div (Rational n1 d1) (Rational n2 d2) = reduce $ Rational (n1*d2) (d1*n2) + +exprational :: Integral a => Rational a -> a -> Rational a +exprational (Rational n d) num = reduce $ if num >= 0 then Rational (n^num) (d^num) else Rational (d^(-num)) (n^(-num)) + +expreal :: Floating a => Integral b => a -> Rational b -> a +expreal num (Rational n d) = num ** (fromIntegral n / fromIntegral d) diff --git a/exercises/practice/rational-numbers/.meta/tests.toml b/exercises/practice/rational-numbers/.meta/tests.toml new file mode 100644 index 000000000..ddea7145c --- /dev/null +++ b/exercises/practice/rational-numbers/.meta/tests.toml @@ -0,0 +1,139 @@ +# 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. + +[0ba4d988-044c-4ed5-9215-4d0bb8d0ae9f] +description = "Arithmetic -> Addition -> Add two positive rational numbers" + +[88ebc342-a2ac-4812-a656-7b664f718b6a] +description = "Arithmetic -> Addition -> Add a positive rational number and a negative rational number" + +[92ed09c2-991e-4082-a602-13557080205c] +description = "Arithmetic -> Addition -> Add two negative rational numbers" + +[6e58999e-3350-45fb-a104-aac7f4a9dd11] +description = "Arithmetic -> Addition -> Add a rational number to its additive inverse" + +[47bba350-9db1-4ab9-b412-4a7e1f72a66e] +description = "Arithmetic -> Subtraction -> Subtract two positive rational numbers" + +[93926e2a-3e82-4aee-98a7-fc33fb328e87] +description = "Arithmetic -> Subtraction -> Subtract a positive rational number and a negative rational number" + +[a965ba45-9b26-442b-bdc7-7728e4b8d4cc] +description = "Arithmetic -> Subtraction -> Subtract two negative rational numbers" + +[0df0e003-f68e-4209-8c6e-6a4e76af5058] +description = "Arithmetic -> Subtraction -> Subtract a rational number from itself" + +[34fde77a-75f4-4204-8050-8d3a937958d3] +description = "Arithmetic -> Multiplication -> Multiply two positive rational numbers" + +[6d015cf0-0ea3-41f1-93de-0b8e38e88bae] +description = "Arithmetic -> Multiplication -> Multiply a negative rational number by a positive rational number" + +[d1bf1b55-954e-41b1-8c92-9fc6beeb76fa] +description = "Arithmetic -> Multiplication -> Multiply two negative rational numbers" + +[a9b8f529-9ec7-4c79-a517-19365d779040] +description = "Arithmetic -> Multiplication -> Multiply a rational number by its reciprocal" + +[d89d6429-22fa-4368-ab04-9e01a44d3b48] +description = "Arithmetic -> Multiplication -> Multiply a rational number by 1" + +[0d95c8b9-1482-4ed7-bac9-b8694fa90145] +description = "Arithmetic -> Multiplication -> Multiply a rational number by 0" + +[1de088f4-64be-4e6e-93fd-5997ae7c9798] +description = "Arithmetic -> Division -> Divide two positive rational numbers" + +[7d7983db-652a-4e66-981a-e921fb38d9a9] +description = "Arithmetic -> Division -> Divide a positive rational number by a negative rational number" + +[1b434d1b-5b38-4cee-aaf5-b9495c399e34] +description = "Arithmetic -> Division -> Divide two negative rational numbers" + +[d81c2ebf-3612-45a6-b4e0-f0d47812bd59] +description = "Arithmetic -> Division -> Divide a rational number by 1" + +[5fee0d8e-5955-4324-acbe-54cdca94ddaa] +description = "Absolute value -> Absolute value of a positive rational number" + +[3cb570b6-c36a-4963-a380-c0834321bcaa] +description = "Absolute value -> Absolute value of a positive rational number with negative numerator and denominator" + +[6a05f9a0-1f6b-470b-8ff7-41af81773f25] +description = "Absolute value -> Absolute value of a negative rational number" + +[5d0f2336-3694-464f-8df9-f5852fda99dd] +description = "Absolute value -> Absolute value of a negative rational number with negative denominator" + +[f8e1ed4b-9dca-47fb-a01e-5311457b3118] +description = "Absolute value -> Absolute value of zero" + +[4a8c939f-f958-473b-9f88-6ad0f83bb4c4] +description = "Absolute value -> Absolute value of a rational number is reduced to lowest terms" + +[ea2ad2af-3dab-41e7-bb9f-bd6819668a84] +description = "Exponentiation of a rational number -> Raise a positive rational number to a positive integer power" + +[8168edd2-0af3-45b1-b03f-72c01332e10a] +description = "Exponentiation of a rational number -> Raise a negative rational number to a positive integer power" + +[c291cfae-cfd8-44f5-aa6c-b175c148a492] +description = "Exponentiation of a rational number -> Raise a positive rational number to a negative integer power" + +[45cb3288-4ae4-4465-9ae5-c129de4fac8e] +description = "Exponentiation of a rational number -> Raise a negative rational number to an even negative integer power" + +[2d47f945-ffe1-4916-a399-c2e8c27d7f72] +description = "Exponentiation of a rational number -> Raise a negative rational number to an odd negative integer power" + +[e2f25b1d-e4de-4102-abc3-c2bb7c4591e4] +description = "Exponentiation of a rational number -> Raise zero to an integer power" + +[431cac50-ab8b-4d58-8e73-319d5404b762] +description = "Exponentiation of a rational number -> Raise one to an integer power" + +[7d164739-d68a-4a9c-b99f-dd77ce5d55e6] +description = "Exponentiation of a rational number -> Raise a positive rational number to the power of zero" + +[eb6bd5f5-f880-4bcd-8103-e736cb6e41d1] +description = "Exponentiation of a rational number -> Raise a negative rational number to the power of zero" + +[30b467dd-c158-46f5-9ffb-c106de2fd6fa] +description = "Exponentiation of a real number to a rational number -> Raise a real number to a positive rational number" + +[6e026bcc-be40-4b7b-ae22-eeaafc5a1789] +description = "Exponentiation of a real number to a rational number -> Raise a real number to a negative rational number" + +[9f866da7-e893-407f-8cd2-ee85d496eec5] +description = "Exponentiation of a real number to a rational number -> Raise a real number to a zero rational number" + +[0a63fbde-b59c-4c26-8237-1e0c73354d0a] +description = "Reduction to lowest terms -> Reduce a positive rational number to lowest terms" + +[5ed6f248-ad8d-4d4e-a545-9146c6727f33] +description = "Reduction to lowest terms -> Reduce places the minus sign on the numerator" + +[f87c2a4e-d29c-496e-a193-318c503e4402] +description = "Reduction to lowest terms -> Reduce a negative rational number to lowest terms" + +[3b92ffc0-5b70-4a43-8885-8acee79cdaaf] +description = "Reduction to lowest terms -> Reduce a rational number with a negative denominator to lowest terms" + +[c9dbd2e6-5ac0-4a41-84c1-48b645b4f663] +description = "Reduction to lowest terms -> Reduce zero to lowest terms" + +[297b45ad-2054-4874-84d4-0358dc1b8887] +description = "Reduction to lowest terms -> Reduce an integer to lowest terms" + +[a73a17fe-fe8c-4a1c-a63b-e7579e333d9e] +description = "Reduction to lowest terms -> Reduce one to lowest terms" diff --git a/exercises/practice/rational-numbers/package.yaml b/exercises/practice/rational-numbers/package.yaml new file mode 100644 index 000000000..61207f272 --- /dev/null +++ b/exercises/practice/rational-numbers/package.yaml @@ -0,0 +1,21 @@ +name: rational-numbers +version: 1.0.0.0 + +dependencies: + - base + +library: + exposed-modules: RationalNumbers + source-dirs: src + ghc-options: -Wall + # dependencies: + # - foo # List here the packages you + # - bar # want to use in your solution. + +tests: + test: + main: Tests.hs + source-dirs: test + dependencies: + - rational-numbers + - hspec diff --git a/exercises/practice/rational-numbers/src/RationalNumbers.hs b/exercises/practice/rational-numbers/src/RationalNumbers.hs new file mode 100644 index 000000000..75de3ca70 --- /dev/null +++ b/exercises/practice/rational-numbers/src/RationalNumbers.hs @@ -0,0 +1,46 @@ +module RationalNumbers +(Rational, + abs, + reduce, + add, + sub, + mul, + div, + exprational, + expreal, + rational) where + +import Prelude hiding (div, abs, Rational) + +-- Data definition ------------------------------------------------------------- +data Rational a = Dummy deriving(Eq, Show) + +rational :: (a, a) -> Rational a +rational = error "You need to implement this function" + +-- unary operators ------------------------------------------------------------- +abs :: Integral a => Rational a -> Rational a +abs = error "You need to implement this function" + +reduce :: Integral a => Rational a -> Rational a +reduce = error "You need to implement this function" + +-- binary operators ------------------------------------------------------------ +add :: Integral a => Rational a -> Rational a -> Rational a +add = error "You need to implement this function" + +sub :: Integral a => Rational a -> Rational a -> Rational a +sub = error "You need to implement this function" + +mul :: Integral a => Rational a -> Rational a -> Rational a +mul = error "You need to implement this function" + +div :: Integral a => Rational a -> Rational a -> Rational a +div = error "You need to implement this function" + +exprational :: Integral a => Rational a -> a -> Rational a +exprational = error "You need to implement this function" + +expreal :: Floating a => Integral b => a -> Rational b -> a +expreal = error "You need to implement this function" + diff --git a/exercises/practice/rational-numbers/stack.yaml b/exercises/practice/rational-numbers/stack.yaml new file mode 100644 index 000000000..115878212 --- /dev/null +++ b/exercises/practice/rational-numbers/stack.yaml @@ -0,0 +1 @@ +resolver: lts-20.18 diff --git a/exercises/practice/rational-numbers/test/Tests.hs b/exercises/practice/rational-numbers/test/Tests.hs new file mode 100644 index 000000000..e07503bcd --- /dev/null +++ b/exercises/practice/rational-numbers/test/Tests.hs @@ -0,0 +1,305 @@ +{-# LANGUAGE RecordWildCards #-} + +import Prelude hiding (abs, div, Rational) +import qualified Prelude as P +import Data.Function (on) +import Data.Foldable (for_) +import Test.Hspec (Spec, describe, it, shouldBe) +import Test.Hspec.Runner (configFailFast, defaultConfig, hspecWith) + +import RationalNumbers + ( Rational + , abs + , reduce + , add + , sub + , mul + , div + , exprational + , expreal + , rational + ) + +main :: IO () +main = hspecWith defaultConfig {configFailFast = True} specs + +specs :: Spec +specs = do + describe "abs" $ for_ absCases $ testA abs + describe "reduce" $ for_ reduceCases $ testA reduce + describe "add" $ for_ addCases $ testB add + describe "sub" $ for_ subCases $ testB sub + describe "mul" $ for_ mulCases $ testB mul + describe "div" $ for_ divCases $ testB div + describe "exprational" $ for_ exprationalCases $ testC exprational + describe "expreal" $ for_ exprealCases $ testD expreal + where + testA f CaseA{..} = it descriptionA $ f (rational number1A) + `shouldBe` rational expectedA + testB f CaseB{..} = it descriptionB $ f (rational number1B) (rational number2B) + `shouldBe` rational expectedB + testC f CaseC{..} = it descriptionC $ f (rational number1C) number2C + `shouldBe` rational expectedC + testD f CaseD{..} = it descriptionD $ f number1D (rational number2D) + `shouldBeAround` expectedD + shouldBeAround = shouldBe `on` roundTo 2 + + roundTo :: Int -> Float -> Float + roundTo n = (/ 10 ^ n) . (fromInteger :: Integer -> Float) . round . (* 10 ^ n) + +data CaseA = CaseA { descriptionA :: String + , number1A :: (Integer, Integer) + , expectedA :: (Integer, Integer) + } + +data CaseB = CaseB { descriptionB :: String + , number1B :: (Integer, Integer) + , number2B :: (Integer, Integer) + , expectedB :: (Integer, Integer) + } + +data CaseC = CaseC { descriptionC :: String + , number1C :: (Integer, Integer) + , number2C :: Integer + , expectedC :: (Integer, Integer) + } + +data CaseD = CaseD { descriptionD :: String + , number1D :: Float + , number2D :: (Integer, Integer) + , expectedD :: Float + } + +absCases :: [CaseA] +absCases = + [ CaseA { descriptionA = "Absolute value of a positive rational number" + , number1A = (1, 2) + , expectedA = (1, 2) + } + , CaseA { descriptionA = "Absolute value of a positive rational number with negative numerator and denominator" + , number1A = (-1, -2) + , expectedA = (1, 2) + } + , CaseA { descriptionA = "Absolute value of a negative rational number" + , number1A = (-1, 2) + , expectedA = (1, 2) + } + , CaseA { descriptionA = "Absolute value of a negative rational number with negative denominator" + , number1A = (1, -2) + , expectedA = (1, 2) + } + , CaseA { descriptionA = "Absolute value of zero" + , number1A = (0, 1) + , expectedA = (0, 1) + } + , CaseA { descriptionA = "Absolute value of a rational number is reduced to lowest terms" + , number1A = (2, 4) + , expectedA = (1, 2) + } + ] + +reduceCases :: [CaseA] +reduceCases = + [ CaseA { descriptionA = "Reduce a positive rational number to lowest terms" + , number1A = (2, 4) + , expectedA = (1, 2) + } + , CaseA { descriptionA = "Reduce places the minus sign on the numerator" + , number1A = (3, -4) + , expectedA = (-3, 4) + } + , CaseA { descriptionA = "Reduce a negative rational number to lowest terms" + , number1A = (-4, 6) + , expectedA = (-2, 3) + } + , CaseA { descriptionA = "Reduce a rational number with a negative denominator to lowest terms" + , number1A = (3, -9) + , expectedA = (-1, 3) + } + , CaseA { descriptionA = "Reduce zero to lowest terms" + , number1A = (0, 6) + , expectedA = (0, 1) + } + , CaseA { descriptionA = "Reduce an integer to lowest terms" + , number1A = (-14, 7) + , expectedA = (-2, 1) + } + , CaseA { descriptionA = "Reduce one to lowest terms" + , number1A = (13, 13) + , expectedA = (1, 1) + } + ] + +addCases :: [CaseB] +addCases = + [ CaseB { descriptionB = "Add two positive rational numbers" + , number1B = (1, 2) + , number2B = (2, 3) + , expectedB = (7, 6) + } + , CaseB { descriptionB = "Add a positive rational number and a negative rational number" + , number1B = (1, 2) + , number2B = (-2, 3) + , expectedB = (-1, 6) + } + , CaseB { descriptionB = "Add two negative rational numbers" + , number1B = (-1, 2) + , number2B = (-2, 3) + , expectedB = (-7, 6) + } + , CaseB { descriptionB = "Add a rational number to its additive inverse" + , number1B = (1, 2) + , number2B = (-1, 2) + , expectedB = (0, 1) + } + ] + +subCases :: [CaseB] +subCases = + [ CaseB { descriptionB = "Subtract two positive rational numbers" + , number1B = (1, 2) + , number2B = (2, 3) + , expectedB = (-1, 6) + } + , CaseB { descriptionB = "Subtract a positive rational number and a negative rational number" + , number1B = (1, 2) + , number2B = (-2, 3) + , expectedB = (7, 6) + } + , CaseB { descriptionB = "Subtract two negative rational numbers" + , number1B = (-1, 2) + , number2B = (-2, 3) + , expectedB = (1, 6) + } + , CaseB { descriptionB = "Subtract a rational number from itself" + , number1B = (1, 2) + , number2B = (1, 2) + , expectedB = (0, 1) + } + ] + +mulCases :: [CaseB] +mulCases = + [ CaseB { descriptionB = "Multiply two positive rational numbers" + , number1B = (1, 2) + , number2B = (2, 3) + , expectedB = (1, 3) + } + , CaseB { descriptionB = "Multiply a negative rational number by a positive rational number" + , number1B = (-1, 2) + , number2B = (2, 3) + , expectedB = (-1, 3) + } + , CaseB { descriptionB = "Multiply two negative rational numbers" + , number1B = (-1, 2) + , number2B = (-2, 3) + , expectedB = (1, 3) + } + , CaseB { descriptionB = "Multiply a rational number by its reciprocal" + , number1B = (1, 2) + , number2B = (2, 1) + , expectedB = (1, 1) + } + , CaseB { descriptionB = "Multiply a rational number by 1" + , number1B = (1, 2) + , number2B = (1, 1) + , expectedB = (1, 2) + } + , CaseB { descriptionB = "Multiply a rational number by 0" + , number1B = (1, 2) + , number2B = (0, 1) + , expectedB = (0, 1) + } + ] + +divCases :: [CaseB] +divCases = + [ CaseB { descriptionB = "Divide two positive rational numbers" + , number1B = (1, 2) + , number2B = (2, 3) + , expectedB = (3, 4) + } + , CaseB { descriptionB = "Divide a positive rational number by a negative rational number" + , number1B = (1, 2) + , number2B = (-2, 3) + , expectedB = (-3, 4) + } + , CaseB { descriptionB = "Divide two negative rational numbers" + , number1B = (-1, 2) + , number2B = (-2, 3) + , expectedB = (3, 4) + } + , CaseB { descriptionB = "Divide a rational number by 1" + , number1B = (1, 2) + , number2B = (1, 1) + , expectedB = (1, 2) + } + ] + +exprationalCases :: [CaseC] +exprationalCases = + [ CaseC { descriptionC = "Raise a positive rational number to a positive integer power" + , number1C = (1, 2) + , number2C = 3 + , expectedC = (1, 8) + } + , CaseC { descriptionC = "Raise a negative rational number to a positive integer power" + , number1C = (-1, 2) + , number2C = 3 + , expectedC = (-1, 8) + } + , CaseC { descriptionC = "Raise a positive rational number to a negative integer power" + , number1C = (3, 5) + , number2C = -2 + , expectedC = (25, 9) + } + , CaseC { descriptionC = "Raise a negative rational number to an even negative integer power" + , number1C = (-3, 5) + , number2C = -2 + , expectedC = (25, 9) + } + , CaseC { descriptionC = "Raise a negative rational number to an odd negative integer power" + , number1C = (-3, 5) + , number2C = -3 + , expectedC = (-125, 27) + } + , CaseC { descriptionC = "Raise zero to an integer power" + , number1C = (0, 1) + , number2C = 5 + , expectedC = (0, 1) + } + , CaseC { descriptionC = "Raise one to an integer power" + , number1C = (1, 1) + , number2C = 4 + , expectedC = (1, 1) + } + , CaseC { descriptionC = "Raise a positive rational number to the power of zero" + , number1C = (1, 2) + , number2C = 0 + , expectedC = (1, 1) + } + , CaseC { descriptionC = "Raise a negative rational number to the power of zero" + , number1C = (-1, 2) + , number2C = 0 + , expectedC = (1, 1) + } + ] + +exprealCases :: [CaseD] +exprealCases = + [ CaseD { descriptionD = "Raise a real number to a positive rational number" + , number1D = 8 + , number2D = (4, 3) + , expectedD = 16.0 + } + , CaseD { descriptionD = "Raise a real number to a negative rational number" + , number1D = 9 + , number2D = (-1, 2) + , expectedD = 0.3333333333333333 + } + , CaseD { descriptionD = "Raise a real number to a zero rational number" + , number1D = 2 + , number2D = (0, 1) + , expectedD = 1.0 + } + ] From 9ec326ec327b42b13880fe77a29f4b889f5dd980 Mon Sep 17 00:00:00 2001 From: tofische Date: Fri, 19 Jan 2024 21:27:04 +0100 Subject: [PATCH 2/3] Incorporated review comments from #1193 --- .../success-standard/src/RationalNumbers.hs | 45 +- .../rational-numbers/src/RationalNumbers.hs | 27 +- .../practice/rational-numbers/test/Tests.hs | 432 ++++++++++-------- 3 files changed, 275 insertions(+), 229 deletions(-) diff --git a/exercises/practice/rational-numbers/.meta/examples/success-standard/src/RationalNumbers.hs b/exercises/practice/rational-numbers/.meta/examples/success-standard/src/RationalNumbers.hs index b98b185d0..1e2f6e7ec 100644 --- a/exercises/practice/rational-numbers/.meta/examples/success-standard/src/RationalNumbers.hs +++ b/exercises/practice/rational-numbers/.meta/examples/success-standard/src/RationalNumbers.hs @@ -1,13 +1,15 @@ module RationalNumbers (Rational, abs, - reduce, + numerator, + denominator, add, sub, mul, div, - exprational, - expreal, + pow, + expRational, + expReal, rational) where import Prelude hiding (div, abs, Rational) @@ -16,35 +18,40 @@ import qualified Prelude as P -- Data definition ------------------------------------------------------------- data Rational a = Rational a a deriving(Eq, Show) -rational :: (a, a) -> Rational a -rational (n, d) = Rational n d +rational :: Integral a => (a, a) -> Rational a +rational (n, d) = Rational (n' `quot` g) (d' `quot` g) + where + g = gcd n d + (n', d') = if d < 0 then (-n, -d) else (n, d) -- unary operators ------------------------------------------------------------- abs :: Integral a => Rational a -> Rational a -abs (Rational n d ) = reduce $ Rational (P.abs n) (P.abs d) +abs (Rational n d) = rational (P.abs n, P.abs d) -reduce :: Integral a => Rational a -> Rational a -reduce (Rational n d) = Rational (n' `quot` g) (d' `quot` g) - where - g = gcd n d - (n', d') = if d < 0 then (-n, -d) else (n, d) +numerator :: Integral a => Rational a -> a +numerator (Rational n _) = n +denominator :: Integral a => Rational a -> a +denominator (Rational _ d) = d -- binary operators ------------------------------------------------------------ add :: Integral a => Rational a -> Rational a -> Rational a -add (Rational n1 d1) (Rational n2 d2) = let dd = d1*d2 in reduce $ Rational (n1*d2 + n2*d1) dd +add (Rational n1 d1) (Rational n2 d2) = let dd = d1*d2 in rational (n1*d2 + n2*d1, dd) sub :: Integral a => Rational a -> Rational a -> Rational a -sub (Rational n1 d1) (Rational n2 d2) = let dd = d1*d2 in reduce $ Rational (n1*d2 - n2*d1) dd +sub (Rational n1 d1) (Rational n2 d2) = let dd = d1*d2 in rational (n1*d2 - n2*d1, dd) mul :: Integral a => Rational a -> Rational a -> Rational a -mul (Rational n1 d1) (Rational n2 d2) = reduce $ Rational (n1*n2) (d1*d2) +mul (Rational n1 d1) (Rational n2 d2) = rational (n1*n2, d1*d2) div :: Integral a => Rational a -> Rational a -> Rational a -div (Rational n1 d1) (Rational n2 d2) = reduce $ Rational (n1*d2) (d1*n2) +div (Rational n1 d1) (Rational n2 d2) = rational (n1*d2, d1*n2) + +pow :: Integral a => Rational a -> a -> Rational a +pow (Rational n d) num = if num >= 0 then rational (n^num, d^num) else rational (d^(-num), n^(-num)) -exprational :: Integral a => Rational a -> a -> Rational a -exprational (Rational n d) num = reduce $ if num >= 0 then Rational (n^num) (d^num) else Rational (d^(-num)) (n^(-num)) +expRational :: Integral a => Floating b => Rational a -> b -> b +expRational (Rational n d) num = (fromIntegral n / fromIntegral d) ** num -expreal :: Floating a => Integral b => a -> Rational b -> a -expreal num (Rational n d) = num ** (fromIntegral n / fromIntegral d) +expReal :: Floating a => Integral b => a -> Rational b -> a +expReal num (Rational n d) = num ** (fromIntegral n / fromIntegral d) diff --git a/exercises/practice/rational-numbers/src/RationalNumbers.hs b/exercises/practice/rational-numbers/src/RationalNumbers.hs index 75de3ca70..7685c2d48 100644 --- a/exercises/practice/rational-numbers/src/RationalNumbers.hs +++ b/exercises/practice/rational-numbers/src/RationalNumbers.hs @@ -1,13 +1,15 @@ module RationalNumbers (Rational, abs, - reduce, + numerator, + denominator, add, sub, mul, div, - exprational, - expreal, + pow, + expRational, + expReal, rational) where import Prelude hiding (div, abs, Rational) @@ -15,15 +17,18 @@ import Prelude hiding (div, abs, Rational) -- Data definition ------------------------------------------------------------- data Rational a = Dummy deriving(Eq, Show) -rational :: (a, a) -> Rational a +rational :: Integral a => (a, a) -> Rational a rational = error "You need to implement this function" -- unary operators ------------------------------------------------------------- abs :: Integral a => Rational a -> Rational a abs = error "You need to implement this function" -reduce :: Integral a => Rational a -> Rational a -reduce = error "You need to implement this function" +numerator :: Integral a => Rational a -> a +numerator = error "You need to implement this function" + +denominator :: Integral a => Rational a -> a +denominator = error "You need to implement this function" -- binary operators ------------------------------------------------------------ add :: Integral a => Rational a -> Rational a -> Rational a @@ -38,9 +43,11 @@ mul = error "You need to implement this function" div :: Integral a => Rational a -> Rational a -> Rational a div = error "You need to implement this function" -exprational :: Integral a => Rational a -> a -> Rational a -exprational = error "You need to implement this function" +pow :: Integral a => Rational a -> a -> Rational a +pow = error "You need to implement this function" -expreal :: Floating a => Integral b => a -> Rational b -> a -expreal = error "You need to implement this function" +expRational :: Integral a => Floating b => Rational a -> b -> b +expRational = error "You need to implement this function" +expReal :: Floating a => Integral b => a -> Rational b -> a +expReal = error "You need to implement this function" diff --git a/exercises/practice/rational-numbers/test/Tests.hs b/exercises/practice/rational-numbers/test/Tests.hs index e07503bcd..9704d7254 100644 --- a/exercises/practice/rational-numbers/test/Tests.hs +++ b/exercises/practice/rational-numbers/test/Tests.hs @@ -1,7 +1,6 @@ {-# LANGUAGE RecordWildCards #-} import Prelude hiding (abs, div, Rational) -import qualified Prelude as P import Data.Function (on) import Data.Foldable (for_) import Test.Hspec (Spec, describe, it, shouldBe) @@ -10,13 +9,15 @@ import Test.Hspec.Runner (configFailFast, defaultConfig, hspecWith) import RationalNumbers ( Rational , abs - , reduce + , numerator + , denominator , add , sub , mul , div - , exprational - , expreal + , pow + , expRational + , expReal , rational ) @@ -25,281 +26,312 @@ main = hspecWith defaultConfig {configFailFast = True} specs specs :: Spec specs = do - describe "abs" $ for_ absCases $ testA abs - describe "reduce" $ for_ reduceCases $ testA reduce + describe "rational" $ for_ rationalCases $ testU rational + describe "numerator" $ for_ numeratorCases $ testU numerator + describe "denominator" $ for_ denominatorCases $ testU denominator + describe "abs" $ for_ absCases $ testU abs describe "add" $ for_ addCases $ testB add describe "sub" $ for_ subCases $ testB sub describe "mul" $ for_ mulCases $ testB mul describe "div" $ for_ divCases $ testB div - describe "exprational" $ for_ exprationalCases $ testC exprational - describe "expreal" $ for_ exprealCases $ testD expreal + describe "pow" $ for_ powCases $ testB pow + describe "expRational" $ for_ expRationalCases $ testR expRational + describe "expReal" $ for_ expRealCases $ testR expReal where - testA f CaseA{..} = it descriptionA $ f (rational number1A) - `shouldBe` rational expectedA - testB f CaseB{..} = it descriptionB $ f (rational number1B) (rational number2B) - `shouldBe` rational expectedB - testC f CaseC{..} = it descriptionC $ f (rational number1C) number2C - `shouldBe` rational expectedC - testD f CaseD{..} = it descriptionD $ f number1D (rational number2D) - `shouldBeAround` expectedD + testU f CaseU{..} = it descriptionU $ f number1U `shouldBe` expectedU + testB f CaseB{..} = it descriptionB $ f number1B number2B `shouldBe` expectedB + testR f CaseB{..} = it descriptionB $ f number1B number2B `shouldBeAround` expectedB shouldBeAround = shouldBe `on` roundTo 2 roundTo :: Int -> Float -> Float roundTo n = (/ 10 ^ n) . (fromInteger :: Integer -> Float) . round . (* 10 ^ n) -data CaseA = CaseA { descriptionA :: String - , number1A :: (Integer, Integer) - , expectedA :: (Integer, Integer) - } +data CaseU a b = CaseU { descriptionU :: String + , number1U :: a + , expectedU :: b + } -data CaseB = CaseB { descriptionB :: String - , number1B :: (Integer, Integer) - , number2B :: (Integer, Integer) - , expectedB :: (Integer, Integer) - } +data CaseB a b c = CaseB { descriptionB :: String + , number1B :: a + , number2B :: b + , expectedB :: c + } -data CaseC = CaseC { descriptionC :: String - , number1C :: (Integer, Integer) - , number2C :: Integer - , expectedC :: (Integer, Integer) - } - -data CaseD = CaseD { descriptionD :: String - , number1D :: Float - , number2D :: (Integer, Integer) - , expectedD :: Float - } - -absCases :: [CaseA] -absCases = - [ CaseA { descriptionA = "Absolute value of a positive rational number" - , number1A = (1, 2) - , expectedA = (1, 2) +rationalCases :: [CaseU (Integer, Integer) (Rational Integer)] +rationalCases = + [ CaseU { descriptionU = "Reduce a positive rational number to lowest terms" + , number1U = (2, 4) + , expectedU = rational (1, 2) + } + , CaseU { descriptionU = "Reduce places the minus sign on the numerator" + , number1U = (3, -4) + , expectedU = rational (-3, 4) } - , CaseA { descriptionA = "Absolute value of a positive rational number with negative numerator and denominator" - , number1A = (-1, -2) - , expectedA = (1, 2) + , CaseU { descriptionU = "Reduce a negative rational number to lowest terms" + , number1U = (-4, 6) + , expectedU = rational (-2, 3) } - , CaseA { descriptionA = "Absolute value of a negative rational number" - , number1A = (-1, 2) - , expectedA = (1, 2) + , CaseU { descriptionU = "Reduce a rational number with a negative denominator to lowest terms" + , number1U = (3, -9) + , expectedU = rational (-1, 3) } - , CaseA { descriptionA = "Absolute value of a negative rational number with negative denominator" - , number1A = (1, -2) - , expectedA = (1, 2) + , CaseU { descriptionU = "Reduce zero to lowest terms" + , number1U = (0, 6) + , expectedU = rational (0, 1) } - , CaseA { descriptionA = "Absolute value of zero" - , number1A = (0, 1) - , expectedA = (0, 1) + , CaseU { descriptionU = "Reduce an integer to lowest terms" + , number1U = (-14, 7) + , expectedU = rational (-2, 1) } - , CaseA { descriptionA = "Absolute value of a rational number is reduced to lowest terms" - , number1A = (2, 4) - , expectedA = (1, 2) + , CaseU { descriptionU = "Reduce one to lowest terms" + , number1U = (13, 13) + , expectedU = rational (1, 1) } ] -reduceCases :: [CaseA] -reduceCases = - [ CaseA { descriptionA = "Reduce a positive rational number to lowest terms" - , number1A = (2, 4) - , expectedA = (1, 2) +numeratorCases :: [CaseU (Rational Integer) Integer] +numeratorCases = + [ CaseU { descriptionU = "Numerator of a reduced rational number" + , number1U = rational (3, -4) + , expectedU = -3 } - , CaseA { descriptionA = "Reduce places the minus sign on the numerator" - , number1A = (3, -4) - , expectedA = (-3, 4) + ] + +denominatorCases :: [CaseU (Rational Integer) Integer] +denominatorCases = + [ CaseU { descriptionU = "Denominator of a reduced rational number" + , number1U = rational (3, -4) + , expectedU = 4 } - , CaseA { descriptionA = "Reduce a negative rational number to lowest terms" - , number1A = (-4, 6) - , expectedA = (-2, 3) + ] + +absCases :: [CaseU (Rational Integer) (Rational Integer)] +absCases = + [ CaseU { descriptionU = "Absolute value of a positive rational number" + , number1U = rational (1, 2) + , expectedU = rational (1, 2) + } + , CaseU { descriptionU = "Absolute value of a positive rational number with negative numerator and denominator" + , number1U = rational (-1, -2) + , expectedU = rational (1, 2) } - , CaseA { descriptionA = "Reduce a rational number with a negative denominator to lowest terms" - , number1A = (3, -9) - , expectedA = (-1, 3) + , CaseU { descriptionU = "Absolute value of a negative rational number" + , number1U = rational (-1, 2) + , expectedU = rational (1, 2) } - , CaseA { descriptionA = "Reduce zero to lowest terms" - , number1A = (0, 6) - , expectedA = (0, 1) + , CaseU { descriptionU = "Absolute value of a negative rational number with negative denominator" + , number1U = rational (1, -2) + , expectedU = rational (1, 2) } - , CaseA { descriptionA = "Reduce an integer to lowest terms" - , number1A = (-14, 7) - , expectedA = (-2, 1) + , CaseU { descriptionU = "Absolute value of zero" + , number1U = rational (0, 1) + , expectedU = rational (0, 1) } - , CaseA { descriptionA = "Reduce one to lowest terms" - , number1A = (13, 13) - , expectedA = (1, 1) + , CaseU { descriptionU = "Absolute value of a rational number is reduced to lowest terms" + , number1U = rational (2, 4) + , expectedU = rational (1, 2) } ] -addCases :: [CaseB] +addCases :: [CaseB (Rational Integer) (Rational Integer) (Rational Integer)] addCases = [ CaseB { descriptionB = "Add two positive rational numbers" - , number1B = (1, 2) - , number2B = (2, 3) - , expectedB = (7, 6) + , number1B = rational (1, 2) + , number2B = rational (2, 3) + , expectedB = rational (7, 6) } , CaseB { descriptionB = "Add a positive rational number and a negative rational number" - , number1B = (1, 2) - , number2B = (-2, 3) - , expectedB = (-1, 6) + , number1B = rational (1, 2) + , number2B = rational (-2, 3) + , expectedB = rational (-1, 6) } , CaseB { descriptionB = "Add two negative rational numbers" - , number1B = (-1, 2) - , number2B = (-2, 3) - , expectedB = (-7, 6) + , number1B = rational (-1, 2) + , number2B = rational (-2, 3) + , expectedB = rational (-7, 6) } , CaseB { descriptionB = "Add a rational number to its additive inverse" - , number1B = (1, 2) - , number2B = (-1, 2) - , expectedB = (0, 1) + , number1B = rational (1, 2) + , number2B = rational (-1, 2) + , expectedB = rational (0, 1) } ] -subCases :: [CaseB] +subCases :: [CaseB (Rational Integer) (Rational Integer) (Rational Integer)] subCases = [ CaseB { descriptionB = "Subtract two positive rational numbers" - , number1B = (1, 2) - , number2B = (2, 3) - , expectedB = (-1, 6) + , number1B = rational (1, 2) + , number2B = rational (2, 3) + , expectedB = rational (-1, 6) } , CaseB { descriptionB = "Subtract a positive rational number and a negative rational number" - , number1B = (1, 2) - , number2B = (-2, 3) - , expectedB = (7, 6) + , number1B = rational (1, 2) + , number2B = rational (-2, 3) + , expectedB = rational (7, 6) } , CaseB { descriptionB = "Subtract two negative rational numbers" - , number1B = (-1, 2) - , number2B = (-2, 3) - , expectedB = (1, 6) + , number1B = rational (-1, 2) + , number2B = rational (-2, 3) + , expectedB = rational (1, 6) } , CaseB { descriptionB = "Subtract a rational number from itself" - , number1B = (1, 2) - , number2B = (1, 2) - , expectedB = (0, 1) + , number1B = rational (1, 2) + , number2B = rational (1, 2) + , expectedB = rational (0, 1) } ] -mulCases :: [CaseB] +mulCases :: [CaseB (Rational Integer) (Rational Integer) (Rational Integer)] mulCases = [ CaseB { descriptionB = "Multiply two positive rational numbers" - , number1B = (1, 2) - , number2B = (2, 3) - , expectedB = (1, 3) + , number1B = rational (1, 2) + , number2B = rational (2, 3) + , expectedB = rational (1, 3) } , CaseB { descriptionB = "Multiply a negative rational number by a positive rational number" - , number1B = (-1, 2) - , number2B = (2, 3) - , expectedB = (-1, 3) + , number1B = rational (-1, 2) + , number2B = rational (2, 3) + , expectedB = rational (-1, 3) } , CaseB { descriptionB = "Multiply two negative rational numbers" - , number1B = (-1, 2) - , number2B = (-2, 3) - , expectedB = (1, 3) + , number1B = rational (-1, 2) + , number2B = rational (-2, 3) + , expectedB = rational (1, 3) } , CaseB { descriptionB = "Multiply a rational number by its reciprocal" - , number1B = (1, 2) - , number2B = (2, 1) - , expectedB = (1, 1) + , number1B = rational (1, 2) + , number2B = rational (2, 1) + , expectedB = rational (1, 1) } , CaseB { descriptionB = "Multiply a rational number by 1" - , number1B = (1, 2) - , number2B = (1, 1) - , expectedB = (1, 2) + , number1B = rational (1, 2) + , number2B = rational (1, 1) + , expectedB = rational (1, 2) } , CaseB { descriptionB = "Multiply a rational number by 0" - , number1B = (1, 2) - , number2B = (0, 1) - , expectedB = (0, 1) + , number1B = rational (1, 2) + , number2B = rational (0, 1) + , expectedB = rational (0, 1) } ] -divCases :: [CaseB] +divCases :: [CaseB (Rational Integer) (Rational Integer) (Rational Integer)] divCases = [ CaseB { descriptionB = "Divide two positive rational numbers" - , number1B = (1, 2) - , number2B = (2, 3) - , expectedB = (3, 4) + , number1B = rational (1, 2) + , number2B = rational (2, 3) + , expectedB = rational (3, 4) } , CaseB { descriptionB = "Divide a positive rational number by a negative rational number" - , number1B = (1, 2) - , number2B = (-2, 3) - , expectedB = (-3, 4) + , number1B = rational (1, 2) + , number2B = rational (-2, 3) + , expectedB = rational (-3, 4) } , CaseB { descriptionB = "Divide two negative rational numbers" - , number1B = (-1, 2) - , number2B = (-2, 3) - , expectedB = (3, 4) + , number1B = rational (-1, 2) + , number2B = rational (-2, 3) + , expectedB = rational (3, 4) } , CaseB { descriptionB = "Divide a rational number by 1" - , number1B = (1, 2) - , number2B = (1, 1) - , expectedB = (1, 2) + , number1B = rational (1, 2) + , number2B = rational (1, 1) + , expectedB = rational (1, 2) + } + ] + +powCases :: [CaseB (Rational Integer) Integer (Rational Integer)] +powCases = + [ CaseB { descriptionB = "Raise a positive rational number to a positive integer power" + , number1B = rational (1, 2) + , number2B = 3 + , expectedB = rational (1, 8) + } + , CaseB { descriptionB = "Raise a negative rational number to a positive integer power" + , number1B = rational (-1, 2) + , number2B = 3 + , expectedB = rational (-1, 8) + } + , CaseB { descriptionB = "Raise a positive rational number to a negative integer power" + , number1B = rational (3, 5) + , number2B = -2 + , expectedB = rational (25, 9) + } + , CaseB { descriptionB = "Raise a negative rational number to an even negative integer power" + , number1B = rational (-3, 5) + , number2B = -2 + , expectedB = rational (25, 9) + } + , CaseB { descriptionB = "Raise a negative rational number to an odd negative integer power" + , number1B = rational (-3, 5) + , number2B = -3 + , expectedB = rational (-125, 27) + } + , CaseB { descriptionB = "Raise zero to an integer power" + , number1B = rational (0, 1) + , number2B = 5 + , expectedB = rational (0, 1) + } + , CaseB { descriptionB = "Raise one to an integer power" + , number1B = rational (1, 1) + , number2B = 4 + , expectedB = rational (1, 1) + } + , CaseB { descriptionB = "Raise a positive rational number to the power of zero" + , number1B = rational (1, 2) + , number2B = 0 + , expectedB = rational (1, 1) + } + , CaseB { descriptionB = "Raise a negative rational number to the power of zero" + , number1B = rational (-1, 2) + , number2B = 0 + , expectedB = rational (1, 1) } ] -exprationalCases :: [CaseC] -exprationalCases = - [ CaseC { descriptionC = "Raise a positive rational number to a positive integer power" - , number1C = (1, 2) - , number2C = 3 - , expectedC = (1, 8) - } - , CaseC { descriptionC = "Raise a negative rational number to a positive integer power" - , number1C = (-1, 2) - , number2C = 3 - , expectedC = (-1, 8) - } - , CaseC { descriptionC = "Raise a positive rational number to a negative integer power" - , number1C = (3, 5) - , number2C = -2 - , expectedC = (25, 9) - } - , CaseC { descriptionC = "Raise a negative rational number to an even negative integer power" - , number1C = (-3, 5) - , number2C = -2 - , expectedC = (25, 9) - } - , CaseC { descriptionC = "Raise a negative rational number to an odd negative integer power" - , number1C = (-3, 5) - , number2C = -3 - , expectedC = (-125, 27) - } - , CaseC { descriptionC = "Raise zero to an integer power" - , number1C = (0, 1) - , number2C = 5 - , expectedC = (0, 1) - } - , CaseC { descriptionC = "Raise one to an integer power" - , number1C = (1, 1) - , number2C = 4 - , expectedC = (1, 1) - } - , CaseC { descriptionC = "Raise a positive rational number to the power of zero" - , number1C = (1, 2) - , number2C = 0 - , expectedC = (1, 1) - } - , CaseC { descriptionC = "Raise a negative rational number to the power of zero" - , number1C = (-1, 2) - , number2C = 0 - , expectedC = (1, 1) +expRationalCases :: [CaseB (Rational Integer) Float Float] +expRationalCases = + [ CaseB { descriptionB = "Raise a rational number to a positive real number bigger than 1" + , number1B = rational (1, 2) + , number2B = 3 + , expectedB = 0.125 + } + , CaseB { descriptionB = "Raise a rational number to a positive real number smaller than 1" + , number1B = rational (1, 2) + , number2B = 0.5 + , expectedB = 0.707 + } + , CaseB { descriptionB = "Raise a rational number to a negative real number bigger than 1" + , number1B = rational (1, 2) + , number2B = -3 + , expectedB = 8.0 + } + , CaseB { descriptionB = "Raise a rational number to a negative real number smaller than 1" + , number1B = rational (1, 2) + , number2B = -0.5 + , expectedB = 1.41 + } + , CaseB { descriptionB = "Raise a rational number to a zero real number" + , number1B = rational (4, 3) + , number2B = 0 + , expectedB = 1.0 } ] -exprealCases :: [CaseD] -exprealCases = - [ CaseD { descriptionD = "Raise a real number to a positive rational number" - , number1D = 8 - , number2D = (4, 3) - , expectedD = 16.0 - } - , CaseD { descriptionD = "Raise a real number to a negative rational number" - , number1D = 9 - , number2D = (-1, 2) - , expectedD = 0.3333333333333333 - } - , CaseD { descriptionD = "Raise a real number to a zero rational number" - , number1D = 2 - , number2D = (0, 1) - , expectedD = 1.0 +expRealCases :: [CaseB Float (Rational Integer) Float] +expRealCases = + [ CaseB { descriptionB = "Raise a real number to a positive rational number" + , number1B = 8 + , number2B = rational (4, 3) + , expectedB = 16.0 + } + , CaseB { descriptionB = "Raise a real number to a negative rational number" + , number1B = 9 + , number2B = rational (-1, 2) + , expectedB = 0.3333333333333333 + } + , CaseB { descriptionB = "Raise a real number to a zero rational number" + , number1B = 2 + , number2B = rational (0, 1) + , expectedB = 1.0 } ] From af7164936c399ac706a6f71f0f3189a35f354494 Mon Sep 17 00:00:00 2001 From: tofische Date: Sat, 20 Jan 2024 15:07:49 +0100 Subject: [PATCH 3/3] Added Affine Cipher exercise --- config.json | 12 ++ .../.docs/instructions.append.md | 37 ++++++ .../affine-cipher/.docs/instructions.md | 74 ++++++++++++ .../practice/affine-cipher/.meta/config.json | 22 ++++ .../examples/success-standard/package.yaml | 18 +++ .../examples/success-standard/src/Affine.hs | 35 ++++++ .../.meta/examples/success-text/package.yaml | 18 +++ .../.meta/examples/success-text/src/Affine.hs | 36 ++++++ .../practice/affine-cipher/.meta/tests.toml | 58 +++++++++ exercises/practice/affine-cipher/package.yaml | 21 ++++ .../practice/affine-cipher/src/Affine.hs | 7 ++ exercises/practice/affine-cipher/stack.yaml | 1 + .../practice/affine-cipher/test/Tests.hs | 112 ++++++++++++++++++ 13 files changed, 451 insertions(+) create mode 100644 exercises/practice/affine-cipher/.docs/instructions.append.md create mode 100644 exercises/practice/affine-cipher/.docs/instructions.md create mode 100644 exercises/practice/affine-cipher/.meta/config.json create mode 100644 exercises/practice/affine-cipher/.meta/examples/success-standard/package.yaml create mode 100644 exercises/practice/affine-cipher/.meta/examples/success-standard/src/Affine.hs create mode 100644 exercises/practice/affine-cipher/.meta/examples/success-text/package.yaml create mode 100644 exercises/practice/affine-cipher/.meta/examples/success-text/src/Affine.hs create mode 100644 exercises/practice/affine-cipher/.meta/tests.toml create mode 100644 exercises/practice/affine-cipher/package.yaml create mode 100644 exercises/practice/affine-cipher/src/Affine.hs create mode 100644 exercises/practice/affine-cipher/stack.yaml create mode 100644 exercises/practice/affine-cipher/test/Tests.hs diff --git a/config.json b/config.json index 4d2b7571c..c9e07ef00 100644 --- a/config.json +++ b/config.json @@ -672,6 +672,18 @@ "lists" ] }, + { + "slug": "affine-cipher", + "name": "Affine Cipher", + "uuid": "31d1b9b8-558c-46ee-8d11-c2217ad5fd77", + "practices": [], + "prerequisites": [], + "difficulty": 6, + "topics": [ + "algorithms", + "strings" + ] + }, { "slug": "alphametics", "name": "Alphametics", diff --git a/exercises/practice/affine-cipher/.docs/instructions.append.md b/exercises/practice/affine-cipher/.docs/instructions.append.md new file mode 100644 index 000000000..f1e8123e3 --- /dev/null +++ b/exercises/practice/affine-cipher/.docs/instructions.append.md @@ -0,0 +1,37 @@ +# Hints + +You need to implement the `decode` and `encode` functions, which decode and encode a `String` using an Affine cipher. +You can use the provided signature if you are unsure about the types, but don't let it restrict your creativity. + +This exercise works with textual data. For historical reasons, Haskell's +`String` type is synonymous with `[Char]`, a list of characters. For more +efficient handling of textual data, the `Text` type can be used. + +As an optional extension to this exercise, you can + +- read about [string types](https://haskell-lang.org/tutorial/string-types) in + Haskell. +- add `- text` to your list of dependencies in package.yaml. +- import `Data.Text` in [the following + way](https://hackernoon.com/4-steps-to-a-better-imports-list-in-haskell-43a3d868273c): + +```haskell +import qualified Data.Text as T +import Data.Text (Text) +``` + +- use the `Text` type e.g. `decode :: Text -> Text` and refer to + `Data.Text` combinators as e.g. `T.pack`. +- look up the documentation for + [`Data.Text`](https://hackage.haskell.org/package/text/docs/Data-Text.html). +- replace all occurrences of `String` with `Text` in Affine.hs, i.e.: + +```haskell +encode :: (Int, Int) -> Text -> Maybe Text +encode key plainText = ... + +decode :: (Int, Int) -> Text -> Maybe Text +decode key cipherText = ... +``` + +This part is entirely optional. diff --git a/exercises/practice/affine-cipher/.docs/instructions.md b/exercises/practice/affine-cipher/.docs/instructions.md new file mode 100644 index 000000000..26ce15342 --- /dev/null +++ b/exercises/practice/affine-cipher/.docs/instructions.md @@ -0,0 +1,74 @@ +# Instructions + +Create an implementation of the affine cipher, an ancient encryption system created in the Middle East. + +The affine cipher is a type of monoalphabetic substitution cipher. +Each character is mapped to its numeric equivalent, encrypted with a mathematical function and then converted to the letter relating to its new numeric value. +Although all monoalphabetic ciphers are weak, the affine cipher is much stronger than the atbash cipher, because it has many more keys. + +[//]: # " monoalphabetic as spelled by Merriam-Webster, compare to polyalphabetic " + +## Encryption + +The encryption function is: + +```text +E(x) = (ai + b) mod m +``` + +Where: + +- `i` is the letter's index from `0` to the length of the alphabet - 1 +- `m` is the length of the alphabet. + For the Roman alphabet `m` is `26`. +- `a` and `b` are integers which make the encryption key + +Values `a` and `m` must be _coprime_ (or, _relatively prime_) for automatic decryption to succeed, i.e., they have number `1` as their only common factor (more information can be found in the [Wikipedia article about coprime integers][coprime-integers]). +In case `a` is not coprime to `m`, your program should indicate that this is an error. +Otherwise it should encrypt or decrypt with the provided key. + +For the purpose of this exercise, digits are valid input but they are not encrypted. +Spaces and punctuation characters are excluded. +Ciphertext is written out in groups of fixed length separated by space, the traditional group size being `5` letters. +This is to make it harder to guess encrypted text based on word boundaries. + +## Decryption + +The decryption function is: + +```text +D(y) = (a^-1)(y - b) mod m +``` + +Where: + +- `y` is the numeric value of an encrypted letter, i.e., `y = E(x)` +- it is important to note that `a^-1` is the modular multiplicative inverse (MMI) of `a mod m` +- the modular multiplicative inverse only exists if `a` and `m` are coprime. + +The MMI of `a` is `x` such that the remainder after dividing `ax` by `m` is `1`: + +```text +ax mod m = 1 +``` + +More information regarding how to find a Modular Multiplicative Inverse and what it means can be found in the [related Wikipedia article][mmi]. + +## General Examples + +- Encrypting `"test"` gives `"ybty"` with the key `a = 5`, `b = 7` +- Decrypting `"ybty"` gives `"test"` with the key `a = 5`, `b = 7` +- Decrypting `"ybty"` gives `"lqul"` with the wrong key `a = 11`, `b = 7` +- Decrypting `"kqlfd jzvgy tpaet icdhm rtwly kqlon ubstx"` gives `"thequickbrownfoxjumpsoverthelazydog"` with the key `a = 19`, `b = 13` +- Encrypting `"test"` with the key `a = 18`, `b = 13` is an error because `18` and `26` are not coprime + +## Example of finding a Modular Multiplicative Inverse (MMI) + +Finding MMI for `a = 15`: + +- `(15 * x) mod 26 = 1` +- `(15 * 7) mod 26 = 1`, ie. `105 mod 26 = 1` +- `7` is the MMI of `15 mod 26` + +[mmi]: https://en.wikipedia.org/wiki/Modular_multiplicative_inverse +[coprime-integers]: https://en.wikipedia.org/wiki/Coprime_integers diff --git a/exercises/practice/affine-cipher/.meta/config.json b/exercises/practice/affine-cipher/.meta/config.json new file mode 100644 index 000000000..6167af070 --- /dev/null +++ b/exercises/practice/affine-cipher/.meta/config.json @@ -0,0 +1,22 @@ +{ + "authors": [], + "files": { + "solution": [ + "src/AffineCipher.hs", + "package.yaml" + ], + "test": [ + "test/Tests.hs" + ], + "example": [ + ".meta/examples/success-standard/src/AffineCipher.hs", + ".meta/examples/success-text/src/AffineCipher.hs" + ], + "invalidator": [ + "stack.yaml" + ] + }, + "blurb": "Create an implementation of the Affine cipher, an ancient encryption algorithm from the Middle East.", + "source": "Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/Affine_cipher" +} diff --git a/exercises/practice/affine-cipher/.meta/examples/success-standard/package.yaml b/exercises/practice/affine-cipher/.meta/examples/success-standard/package.yaml new file mode 100644 index 000000000..357d87ed9 --- /dev/null +++ b/exercises/practice/affine-cipher/.meta/examples/success-standard/package.yaml @@ -0,0 +1,18 @@ +name: affine-cipher + +dependencies: + - base + +library: + exposed-modules: Affine + source-dirs: src + dependencies: + - split + +tests: + test: + main: Tests.hs + source-dirs: test + dependencies: + - affine-cipher + - hspec diff --git a/exercises/practice/affine-cipher/.meta/examples/success-standard/src/Affine.hs b/exercises/practice/affine-cipher/.meta/examples/success-standard/src/Affine.hs new file mode 100644 index 000000000..408b6fe9d --- /dev/null +++ b/exercises/practice/affine-cipher/.meta/examples/success-standard/src/Affine.hs @@ -0,0 +1,35 @@ +module Affine (decode, encode) where + +import Data.Char (chr, isAlphaNum, isDigit, ord, toLower) +import Data.List (find) +import Data.List.Split (chunksOf) + +alphabetStart :: Int +alphabetStart = ord 'a' + +alphabetLength :: Int +alphabetLength = 26 + +encode :: (Int, Int) -> String -> Maybe String +encode (keyA, keyB) cipherText = processText <$> findMMI keyA + where + processText mmi = unwords $ chunksOf 5 $ translateString (func mmi) cipherText + func _ idx = (keyA*idx + keyB) `mod` alphabetLength + +decode :: (Int, Int) -> String -> Maybe String +decode (keyA, keyB) plainText = processText <$> findMMI keyA + where + processText mmi = translateString (func mmi) plainText + func mmi idx = mmi*(idx-keyB) `mod` alphabetLength + +translateString :: (Int -> Int) -> String -> String +translateString func text = map translateChar $ filter isAlphaNum text + where + translateChar ch = if isDigit ch then ch else fromIndex $ func $ toIndex $ toLower ch + fromIndex idx = chr (idx + alphabetStart) + toIndex ch = ord ch - alphabetStart + +findMMI :: Int -> Maybe Int +findMMI keyA = find isMMI [1..alphabetLength] + where + isMMI x = keyA*x `mod` alphabetLength == 1 diff --git a/exercises/practice/affine-cipher/.meta/examples/success-text/package.yaml b/exercises/practice/affine-cipher/.meta/examples/success-text/package.yaml new file mode 100644 index 000000000..c50533c9d --- /dev/null +++ b/exercises/practice/affine-cipher/.meta/examples/success-text/package.yaml @@ -0,0 +1,18 @@ +name: affine-cipher + +dependencies: + - base + +library: + exposed-modules: Affine + source-dirs: src + dependencies: + - text + +tests: + test: + main: Tests.hs + source-dirs: test + dependencies: + - affine-cipher + - hspec diff --git a/exercises/practice/affine-cipher/.meta/examples/success-text/src/Affine.hs b/exercises/practice/affine-cipher/.meta/examples/success-text/src/Affine.hs new file mode 100644 index 000000000..1231f35c2 --- /dev/null +++ b/exercises/practice/affine-cipher/.meta/examples/success-text/src/Affine.hs @@ -0,0 +1,36 @@ +module Affine (decode, encode) where + +import Data.Char +import Data.List (find) +import qualified Data.Text as T +import Data.Text (Text) + +alphabetStart :: Int +alphabetStart = ord 'a' + +alphabetLength :: Int +alphabetLength = 26 + +encode :: (Int, Int) -> Text -> Maybe Text +encode (keyA, keyB) cipherText = processText <$> findMMI keyA + where + processText mmi = T.unwords $ T.chunksOf 5 $ translateString (func mmi) cipherText + func _ idx = (keyA*idx + keyB) `mod` alphabetLength + +decode :: (Int, Int) -> Text -> Maybe Text +decode (keyA, keyB) plainText = processText <$> findMMI keyA + where + processText mmi = translateString (func mmi) plainText + func mmi idx = mmi*(idx-keyB) `mod` alphabetLength + +translateString :: (Int -> Int) -> Text -> Text +translateString func text = T.map translateChar $ T.filter isAlphaNum text + where + translateChar ch = if isDigit ch then ch else fromIndex $ func $ toIndex $ toLower ch + fromIndex idx = chr (idx + alphabetStart) + toIndex ch = ord ch - alphabetStart + +findMMI :: Int -> Maybe Int +findMMI keyA = find isMMI [1..alphabetLength] + where + isMMI x = keyA*x `mod` alphabetLength == 1 diff --git a/exercises/practice/affine-cipher/.meta/tests.toml b/exercises/practice/affine-cipher/.meta/tests.toml new file mode 100644 index 000000000..07cce7c77 --- /dev/null +++ b/exercises/practice/affine-cipher/.meta/tests.toml @@ -0,0 +1,58 @@ +# 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. + +[2ee1d9af-1c43-416c-b41b-cefd7d4d2b2a] +description = "encode -> encode yes" + +[785bade9-e98b-4d4f-a5b0-087ba3d7de4b] +description = "encode -> encode no" + +[2854851c-48fb-40d8-9bf6-8f192ed25054] +description = "encode -> encode OMG" + +[bc0c1244-b544-49dd-9777-13a770be1bad] +description = "encode -> encode O M G" + +[381a1a20-b74a-46ce-9277-3778625c9e27] +description = "encode -> encode mindblowingly" + +[6686f4e2-753b-47d4-9715-876fdc59029d] +description = "encode -> encode numbers" + +[ae23d5bd-30a8-44b6-afbe-23c8c0c7faa3] +description = "encode -> encode deep thought" + +[c93a8a4d-426c-42ef-9610-76ded6f7ef57] +description = "encode -> encode all the letters" + +[0673638a-4375-40bd-871c-fb6a2c28effb] +description = "encode -> encode with a not coprime to m" + +[3f0ac7e2-ec0e-4a79-949e-95e414953438] +description = "decode -> decode exercism" + +[241ee64d-5a47-4092-a5d7-7939d259e077] +description = "decode -> decode a sentence" + +[33fb16a1-765a-496f-907f-12e644837f5e] +description = "decode -> decode numbers" + +[20bc9dce-c5ec-4db6-a3f1-845c776bcbf7] +description = "decode -> decode all the letters" + +[623e78c0-922d-49c5-8702-227a3e8eaf81] +description = "decode -> decode with no spaces in input" + +[58fd5c2a-1fd9-4563-a80a-71cff200f26f] +description = "decode -> decode with too many spaces" + +[b004626f-c186-4af9-a3f4-58f74cdb86d5] +description = "decode -> decode with a not coprime to m" diff --git a/exercises/practice/affine-cipher/package.yaml b/exercises/practice/affine-cipher/package.yaml new file mode 100644 index 000000000..166cbed03 --- /dev/null +++ b/exercises/practice/affine-cipher/package.yaml @@ -0,0 +1,21 @@ +name: affine-cipher +version: 1.0.0.0 + +dependencies: + - base + +library: + exposed-modules: Affine + source-dirs: src + ghc-options: -Wall + # dependencies: + # - foo # List here the packages you + # - bar # want to use in your solution. + +tests: + test: + main: Tests.hs + source-dirs: test + dependencies: + - affine-cipher + - hspec diff --git a/exercises/practice/affine-cipher/src/Affine.hs b/exercises/practice/affine-cipher/src/Affine.hs new file mode 100644 index 000000000..0ff96cf49 --- /dev/null +++ b/exercises/practice/affine-cipher/src/Affine.hs @@ -0,0 +1,7 @@ +module Affine (decode, encode) where + +decode :: (Int, Int) -> String -> Maybe String +decode key cipherText = error "You need to implement this function." + +encode :: (Int, Int) -> String -> Maybe String +encode key plainText = error "You need to implement this function." diff --git a/exercises/practice/affine-cipher/stack.yaml b/exercises/practice/affine-cipher/stack.yaml new file mode 100644 index 000000000..115878212 --- /dev/null +++ b/exercises/practice/affine-cipher/stack.yaml @@ -0,0 +1 @@ +resolver: lts-20.18 diff --git a/exercises/practice/affine-cipher/test/Tests.hs b/exercises/practice/affine-cipher/test/Tests.hs new file mode 100644 index 000000000..1c014f24a --- /dev/null +++ b/exercises/practice/affine-cipher/test/Tests.hs @@ -0,0 +1,112 @@ +{-# LANGUAGE RecordWildCards #-} + +import Data.Foldable (for_) +import Test.Hspec (Spec, describe, it, shouldBe) +import Test.Hspec.Runner (configFailFast, defaultConfig, hspecWith) +import Data.String (fromString) + +import Affine (encode, decode) + +main :: IO () +main = hspecWith defaultConfig {configFailFast = True} specs + +specs :: Spec +specs = do + describe "encode" $ for_ encodeCases $ test encode + describe "decode" $ for_ decodeCases $ test decode + where + test f Case{..} = it description $ f key (fromString phrase) `shouldBe` (fromString <$> expected) + +data Case = Case { description :: String + , phrase :: String + , key :: (Int, Int) + , expected :: Maybe String + } + +encodeCases :: [Case] +encodeCases = + [ Case { description = "encode yes" + , phrase = "yes" + , key = (5, 7) + , expected = Just "xbt" + } + , Case { description = "encode no" + , phrase = "no" + , key = (15, 18) + , expected = Just "fu" + } + , Case { description = "encode OMG" + , phrase = "OMG" + , key = (21, 3) + , expected = Just "lvz" + } + , Case { description = "encode spaces" + , phrase = "O M G" + , key = (25, 47) + , expected = Just "hjp" + } + , Case { description = "encode mindblowingly" + , phrase = "mindblowingly" + , key = (11, 15) + , expected = Just "rzcwa gnxzc dgt" + } + , Case { description = "encode numbers" + , phrase = "Testing,1 2 3, testing." + , key = (3, 4) + , expected = Just "jqgjc rw123 jqgjc rw" + } + , Case { description = "encode deep thought" + , phrase = "Truth is fiction." + , key = (5, 17) + , expected = Just "iynia fdqfb ifje" + } + , Case { description = "encode all the letters" + , phrase = "The quick brown fox jumps over the lazy dog." + , key = (17, 33) + , expected = Just "swxtj npvyk lruol iejdc blaxk swxmh qzglf" + } + , Case { description = "encode with a not coprime to m" + , phrase = "This is a test." + , key = (6, 17) + , expected = Nothing + } + ] + +decodeCases :: [Case] +decodeCases = + [ Case { description = "decode exercism" + , phrase = "tytgn fjr" + , key = (3, 7) + , expected = Just "exercism" + } + , Case { description = "decode a sentence" + , phrase = "qdwju nqcro muwhn odqun oppmd aunwd o" + , key = (19, 16) + , expected = Just "anobstacleisoftenasteppingstone" + } + , Case { description = "decode numbers" + , phrase = "odpoz ub123 odpoz ub" + , key = (25, 7) + , expected = Just "testing123testing" + } + , Case { description = "decode all the letters" + , phrase = "swxtj npvyk lruol iejdc blaxk swxmh qzglf" + , key = (17, 33) + , expected = Just "thequickbrownfoxjumpsoverthelazydog" + } + , Case { description = "decode with no spaces in input" + , phrase = "swxtjnpvyklruoliejdcblaxkswxmhqzglf" + , key = (17, 33) + , expected = Just "thequickbrownfoxjumpsoverthelazydog" + } + , Case { description = "decode with too many spaces" + , phrase = "vszzm cly yd cg qdp" + , key = (15, 16) + , expected = Just "jollygreengiant" + } + , Case { description = "decode with a not coprime to m" + , phrase = "Test" + , key = (13, 5) + , expected = Nothing + } + ]