diff --git a/config.json b/config.json index dd674ea..4d4625f 100644 --- a/config.json +++ b/config.json @@ -391,6 +391,14 @@ "prerequisites": [], "difficulty": 4 }, + { + "slug": "luhn", + "name": "Luhn", + "uuid": "209e0108-01b7-43e3-bdaa-a3b30ca6b5a2", + "practices": [], + "prerequisites": [], + "difficulty": 5 + }, { "slug": "scrabble-score", "name": "Scrabble Score", diff --git a/exercises/practice/luhn/.docs/instructions.md b/exercises/practice/luhn/.docs/instructions.md new file mode 100644 index 0000000..8cbe791 --- /dev/null +++ b/exercises/practice/luhn/.docs/instructions.md @@ -0,0 +1,64 @@ +# Instructions + +Given a number determine whether or not it is valid per the Luhn formula. + +The [Luhn algorithm][luhn] is a simple checksum formula used to validate a variety of identification numbers, such as credit card numbers and Canadian Social Insurance Numbers. + +The task is to check if a given string is valid. + +## Validating a Number + +Strings of length 1 or less are not valid. +Spaces are allowed in the input, but they should be stripped before checking. +All other non-digit characters are disallowed. + +### Example 1: valid credit card number + +```text +4539 3195 0343 6467 +``` + +The first step of the Luhn algorithm is to double every second digit, starting from the right. +We will be doubling + +```text +4_3_ 3_9_ 0_4_ 6_6_ +``` + +If doubling the number results in a number greater than 9 then subtract 9 from the product. +The results of our doubling: + +```text +8569 6195 0383 3437 +``` + +Then sum all of the digits: + +```text +8+5+6+9+6+1+9+5+0+3+8+3+3+4+3+7 = 80 +``` + +If the sum is evenly divisible by 10, then the number is valid. +This number is valid! + +### Example 2: invalid credit card number + +```text +8273 1232 7352 0569 +``` + +Double the second digits, starting from the right + +```text +7253 2262 5312 0539 +``` + +Sum the digits + +```text +7+2+5+3+2+2+6+2+5+3+1+2+0+5+3+9 = 57 +``` + +57 is not evenly divisible by 10, so this number is not valid. + +[luhn]: https://en.wikipedia.org/wiki/Luhn_algorithm diff --git a/exercises/practice/luhn/.meta/config.json b/exercises/practice/luhn/.meta/config.json new file mode 100644 index 0000000..c4fc153 --- /dev/null +++ b/exercises/practice/luhn/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "SimaDovakin" + ], + "files": { + "solution": [ + "luhn.u" + ], + "test": [ + "luhn.test.u" + ], + "example": [ + ".meta/examples/luhn.example.u" + ] + }, + "blurb": "Given a number determine whether or not it is valid per the Luhn formula.", + "source": "The Luhn Algorithm on Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/Luhn_algorithm" +} diff --git a/exercises/practice/luhn/.meta/examples/luhn.example.u b/exercises/practice/luhn/.meta/examples/luhn.example.u new file mode 100644 index 0000000..1cb1045 --- /dev/null +++ b/exercises/practice/luhn/.meta/examples/luhn.example.u @@ -0,0 +1,43 @@ +luhn.isValid : Text -> Boolean +luhn.isValid string = + charList = toCharList string + digitCharList = List.filter (Class.is Class.digit) charList + match (charList |> any (not << isValidChar), 1 >= size digitCharList) with + (false, false) -> digitCharList + |> map toDigit + |> checkSum + |> (s -> 0 == mod s 10) + (_, _) -> false + +luhn.checkSum : [Optional Nat] -> Nat +luhn.checkSum digits = + digits + |> foldRight accumulateDigits (0, 0) + |> at2 + +luhn.isValidChar : Char -> Boolean +luhn.isValidChar char = + charClass = Class.or Class.digit Class.whitespace + Class.is charClass char + +luhn.toDigit : Char -> Optional Nat +luhn.toDigit char = + codePoint = toNat char + match inRange 48 58 codePoint with + true -> Some (codePoint - 48) + false -> None + +luhn.doubleDigit : Nat -> Nat +luhn.doubleDigit digit = + doubledDigit = digit * 2 + match doubledDigit > 9 with + true -> doubledDigit - 9 + false -> doubledDigit + +luhn.accumulateDigits : Optional Nat -> (Nat, Nat) -> (Nat, Nat) +luhn.accumulateDigits digit acc = + (index, totalSum) = acc + match (digit, 1 == mod index 2) with + (Some d, true) -> (index + 1, totalSum + doubleDigit d) + (Some d, false) -> (index + 1, totalSum + d) + (_, _) -> (index, totalSum) diff --git a/exercises/practice/luhn/.meta/testAnnotation.json b/exercises/practice/luhn/.meta/testAnnotation.json new file mode 100644 index 0000000..d195ef5 --- /dev/null +++ b/exercises/practice/luhn/.meta/testAnnotation.json @@ -0,0 +1,90 @@ +[ + { + "name": "luhn.isValid.tests.ex1", + "test_code": "expect (false == luhn.isValid \"1\")\n |> Test.label \"single digit strings can not be valid\"" + }, + { + "name": "luhn.isValid.tests.ex2", + "test_code": "expect (false == luhn.isValid \"0\")\n |> Test.label \"a single zero is invalid\"" + }, + { + "name": "luhn.isValid.tests.ex3", + "test_code": "expect (true == luhn.isValid \"059\")\n |> Test.label \"a simple valid SIN that remains valid if reversed\"" + }, + { + "name": "luhn.isValid.tests.ex4", + "test_code": "expect (true == luhn.isValid \"59\")\n |> Test.label \"a simple valid SIN that becomes invalid if reversed\"" + }, + { + "name": "luhn.isValid.tests.ex5", + "test_code": "expect (true == luhn.isValid \"055 444 285\")\n |> Test.label \"a valid Canadian SIN\"" + }, + { + "name": "luhn.isValid.tests.ex6", + "test_code": "expect (false == luhn.isValid \"055 444 286\")\n |> Test.label \"invalid Canadian SIN\"" + }, + { + "name": "luhn.isValid.tests.ex7", + "test_code": "expect (false == luhn.isValid \"8273 1232 7352 0569\")\n |> Test.label \"invalid credit card\"" + }, + { + "name": "luhn.isValid.tests.ex8", + "test_code": "expect (false == luhn.isValid \"1 2345 6789 1234 5678 9012\")\n |> Test.label \"invalid long number with an even remainder\"" + }, + { + "name": "luhn.isValid.tests.ex9", + "test_code": "expect (false == luhn.isValid \"1 2345 6789 1234 5678 9013\")\n |> Test.label \"invalid long number with a remainder divisible by 5\"" + }, + { + "name": "luhn.isValid.tests.ex10", + "test_code": "expect (true == luhn.isValid \"095 245 88\")\n |> Test.label \"valid number with an even number of digits\"" + }, + { + "name": "luhn.isValid.tests.ex11", + "test_code": "expect (true == luhn.isValid \"234 567 891 234\")\n |> Test.label \"valid number with an odd number of spaces\"" + }, + { + "name": "luhn.isValid.tests.ex12", + "test_code": "expect (false == luhn.isValid \"059a\")\n |> Test.label \"valid strings with a non-digit added at the end become invalid\"" + }, + { + "name": "luhn.isValid.tests.ex13", + "test_code": "expect (false == luhn.isValid \"055-444-285\")\n |> Test.label \"valid strings with punctuation included become invalid\"" + }, + { + "name": "luhn.isValid.tests.ex14", + "test_code": "expect (false == luhn.isValid \"055# 444$ 285\")\n |> Test.label \"valid strings with symbols included become invalid\"" + }, + { + "name": "luhn.isValid.tests.ex15", + "test_code": "expect (false == luhn.isValid \" 0\")\n |> Test.label \"single zero with space is invalid\"" + }, + { + "name": "luhn.isValid.tests.ex16", + "test_code": "expect (true == luhn.isValid \"0000 0\")\n |> Test.label \"more than a single zero is valid\"" + }, + { + "name": "luhn.isValid.tests.ex17", + "test_code": "expect (true == luhn.isValid \"091\")\n |> Test.label \"input digit 9 is correctly converted to output digit 9\"" + }, + { + "name": "luhn.isValid.tests.ex18", + "test_code": "expect (true == luhn.isValid \"9999999999 9999999999 9999999999 9999999999\")\n |> Test.label \"very long input is valid\"" + }, + { + "name": "luhn.isValid.tests.ex19", + "test_code": "expect (true == luhn.isValid \"109\")\n |> Test.label \"valid luhn with an odd number of digits and non zero first digit\"" + }, + { + "name": "luhn.isValid.tests.ex20", + "test_code": "expect (false == luhn.isValid \"055b 444 285\")\n |> Test.label \"using ascii value for non-doubled non-digit isn't allowed\"" + }, + { + "name": "luhn.isValid.tests.ex21", + "test_code": "expect (false == luhn.isValid \":9\")\n |> Test.label \"using ascii value for doubled non-digit isn't allowed\"" + }, + { + "name": "luhn.isValid.tests.ex22", + "test_code": "expect (false == luhn.isValid \"59%59\")\n |> Test.label \"non-numeric, non-space char in the middle with a sum that's divisible by 10 isn't allowed\"" + } +] diff --git a/exercises/practice/luhn/.meta/testLoader.md b/exercises/practice/luhn/.meta/testLoader.md new file mode 100644 index 0000000..cdad641 --- /dev/null +++ b/exercises/practice/luhn/.meta/testLoader.md @@ -0,0 +1,9 @@ +# Testing transcript for luhn exercise + +```ucm +.> load ./luhn.u +.> add +.> load ./luhn.test.u +.> add +.> move.term luhn.tests tests +``` diff --git a/exercises/practice/luhn/.meta/tests.toml b/exercises/practice/luhn/.meta/tests.toml new file mode 100644 index 0000000..c0be0c4 --- /dev/null +++ b/exercises/practice/luhn/.meta/tests.toml @@ -0,0 +1,76 @@ +# 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. + +[792a7082-feb7-48c7-b88b-bbfec160865e] +description = "single digit strings can not be valid" + +[698a7924-64d4-4d89-8daa-32e1aadc271e] +description = "a single zero is invalid" + +[73c2f62b-9b10-4c9f-9a04-83cee7367965] +description = "a simple valid SIN that remains valid if reversed" + +[9369092e-b095-439f-948d-498bd076be11] +description = "a simple valid SIN that becomes invalid if reversed" + +[8f9f2350-1faf-4008-ba84-85cbb93ffeca] +description = "a valid Canadian SIN" + +[1cdcf269-6560-44fc-91f6-5819a7548737] +description = "invalid Canadian SIN" + +[656c48c1-34e8-4e60-9a5a-aad8a367810a] +description = "invalid credit card" + +[20e67fad-2121-43ed-99a8-14b5b856adb9] +description = "invalid long number with an even remainder" + +[7e7c9fc1-d994-457c-811e-d390d52fba5e] +description = "invalid long number with a remainder divisible by 5" + +[ad2a0c5f-84ed-4e5b-95da-6011d6f4f0aa] +description = "valid number with an even number of digits" + +[ef081c06-a41f-4761-8492-385e13c8202d] +description = "valid number with an odd number of spaces" + +[bef66f64-6100-4cbb-8f94-4c9713c5e5b2] +description = "valid strings with a non-digit added at the end become invalid" + +[2177e225-9ce7-40f6-b55d-fa420e62938e] +description = "valid strings with punctuation included become invalid" + +[ebf04f27-9698-45e1-9afe-7e0851d0fe8d] +description = "valid strings with symbols included become invalid" + +[08195c5e-ce7f-422c-a5eb-3e45fece68ba] +description = "single zero with space is invalid" + +[12e63a3c-f866-4a79-8c14-b359fc386091] +description = "more than a single zero is valid" + +[ab56fa80-5de8-4735-8a4a-14dae588663e] +description = "input digit 9 is correctly converted to output digit 9" + +[b9887ee8-8337-46c5-bc45-3bcab51bc36f] +description = "very long input is valid" + +[8a7c0e24-85ea-4154-9cf1-c2db90eabc08] +description = "valid luhn with an odd number of digits and non zero first digit" + +[39a06a5a-5bad-4e0f-b215-b042d46209b1] +description = "using ascii value for non-doubled non-digit isn't allowed" + +[f94cf191-a62f-4868-bc72-7253114aa157] +description = "using ascii value for doubled non-digit isn't allowed" + +[8b72ad26-c8be-49a2-b99c-bcc3bf631b33] +description = "non-numeric, non-space char in the middle with a sum that's divisible by 10 isn't allowed" diff --git a/exercises/practice/luhn/luhn.test.u b/exercises/practice/luhn/luhn.test.u new file mode 100644 index 0000000..fc83624 --- /dev/null +++ b/exercises/practice/luhn/luhn.test.u @@ -0,0 +1,112 @@ +luhn.isValid.tests.ex1 = + expect (false == luhn.isValid "1") + |> Test.label "single digit strings can not be valid" + +luhn.isValid.tests.ex2 = + expect (false == luhn.isValid "0") + |> Test.label "a single zero is invalid" + +luhn.isValid.tests.ex3 = + expect (true == luhn.isValid "059") + |> Test.label "a simple valid SIN that remains valid if reversed" + +luhn.isValid.tests.ex4 = + expect (true == luhn.isValid "59") + |> Test.label "a simple valid SIN that becomes invalid if reversed" + +luhn.isValid.tests.ex5 = + expect (true == luhn.isValid "055 444 285") + |> Test.label "a valid Canadian SIN" + +luhn.isValid.tests.ex6 = + expect (false == luhn.isValid "055 444 286") + |> Test.label "invalid Canadian SIN" + +luhn.isValid.tests.ex7 = + expect (false == luhn.isValid "8273 1232 7352 0569") + |> Test.label "invalid credit card" + +luhn.isValid.tests.ex8 = + expect (false == luhn.isValid "1 2345 6789 1234 5678 9012") + |> Test.label "invalid long number with an even remainder" + +luhn.isValid.tests.ex9 = + expect (false == luhn.isValid "1 2345 6789 1234 5678 9013") + |> Test.label "invalid long number with a remainder divisible by 5" + +luhn.isValid.tests.ex10 = + expect (true == luhn.isValid "095 245 88") + |> Test.label "valid number with an even number of digits" + +luhn.isValid.tests.ex11 = + expect (true == luhn.isValid "234 567 891 234") + |> Test.label "valid number with an odd number of spaces" + +luhn.isValid.tests.ex12 = + expect (false == luhn.isValid "059a") + |> Test.label "valid strings with a non-digit added at the end become invalid" + +luhn.isValid.tests.ex13 = + expect (false == luhn.isValid "055-444-285") + |> Test.label "valid strings with punctuation included become invalid" + +luhn.isValid.tests.ex14 = + expect (false == luhn.isValid "055# 444$ 285") + |> Test.label "valid strings with symbols included become invalid" + +luhn.isValid.tests.ex15 = + expect (false == luhn.isValid " 0") + |> Test.label "single zero with space is invalid" + +luhn.isValid.tests.ex16 = + expect (true == luhn.isValid "0000 0") + |> Test.label "more than a single zero is valid" + +luhn.isValid.tests.ex17 = + expect (true == luhn.isValid "091") + |> Test.label "input digit 9 is correctly converted to output digit 9" + +luhn.isValid.tests.ex18 = + expect (true == luhn.isValid "9999999999 9999999999 9999999999 9999999999") + |> Test.label "very long input is valid" + +luhn.isValid.tests.ex19 = + expect (true == luhn.isValid "109") + |> Test.label "valid luhn with an odd number of digits and non zero first digit" + +luhn.isValid.tests.ex20 = + expect (false == luhn.isValid "055b 444 285") + |> Test.label "using ascii value for non-doubled non-digit isn't allowed" + +luhn.isValid.tests.ex21 = + expect (false == luhn.isValid ":9") + |> Test.label "using ascii value for doubled non-digit isn't allowed" + +luhn.isValid.tests.ex22 = + expect (false == luhn.isValid "59%59") + |> Test.label "non-numeric, non-space char in the middle with a sum that's divisible by 10 isn't allowed" + +test> luhn.tests = runAll [ + luhn.isValid.tests.ex1, + luhn.isValid.tests.ex2, + luhn.isValid.tests.ex3, + luhn.isValid.tests.ex4, + luhn.isValid.tests.ex5, + luhn.isValid.tests.ex6, + luhn.isValid.tests.ex7, + luhn.isValid.tests.ex8, + luhn.isValid.tests.ex9, + luhn.isValid.tests.ex10, + luhn.isValid.tests.ex11, + luhn.isValid.tests.ex12, + luhn.isValid.tests.ex13, + luhn.isValid.tests.ex14, + luhn.isValid.tests.ex15, + luhn.isValid.tests.ex16, + luhn.isValid.tests.ex17, + luhn.isValid.tests.ex18, + luhn.isValid.tests.ex19, + luhn.isValid.tests.ex20, + luhn.isValid.tests.ex21, + luhn.isValid.tests.ex22 +] diff --git a/exercises/practice/luhn/luhn.u b/exercises/practice/luhn/luhn.u new file mode 100644 index 0000000..38c0a49 --- /dev/null +++ b/exercises/practice/luhn/luhn.u @@ -0,0 +1,2 @@ +luhn.isValid : Text -> Boolean +luhn.isValid string = todo "implement isValid"