Skip to content

Commit a27178a

Browse files
authored
Merge pull request #127 from SimaDovakin/luhn-exercise
luhn-exercise: added new exercise
2 parents 16be8a7 + 9237a62 commit a27178a

File tree

9 files changed

+423
-0
lines changed

9 files changed

+423
-0
lines changed

config.json

+8
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,14 @@
391391
"prerequisites": [],
392392
"difficulty": 4
393393
},
394+
{
395+
"slug": "luhn",
396+
"name": "Luhn",
397+
"uuid": "209e0108-01b7-43e3-bdaa-a3b30ca6b5a2",
398+
"practices": [],
399+
"prerequisites": [],
400+
"difficulty": 5
401+
},
394402
{
395403
"slug": "scrabble-score",
396404
"name": "Scrabble Score",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Instructions
2+
3+
Given a number determine whether or not it is valid per the Luhn formula.
4+
5+
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.
6+
7+
The task is to check if a given string is valid.
8+
9+
## Validating a Number
10+
11+
Strings of length 1 or less are not valid.
12+
Spaces are allowed in the input, but they should be stripped before checking.
13+
All other non-digit characters are disallowed.
14+
15+
### Example 1: valid credit card number
16+
17+
```text
18+
4539 3195 0343 6467
19+
```
20+
21+
The first step of the Luhn algorithm is to double every second digit, starting from the right.
22+
We will be doubling
23+
24+
```text
25+
4_3_ 3_9_ 0_4_ 6_6_
26+
```
27+
28+
If doubling the number results in a number greater than 9 then subtract 9 from the product.
29+
The results of our doubling:
30+
31+
```text
32+
8569 6195 0383 3437
33+
```
34+
35+
Then sum all of the digits:
36+
37+
```text
38+
8+5+6+9+6+1+9+5+0+3+8+3+3+4+3+7 = 80
39+
```
40+
41+
If the sum is evenly divisible by 10, then the number is valid.
42+
This number is valid!
43+
44+
### Example 2: invalid credit card number
45+
46+
```text
47+
8273 1232 7352 0569
48+
```
49+
50+
Double the second digits, starting from the right
51+
52+
```text
53+
7253 2262 5312 0539
54+
```
55+
56+
Sum the digits
57+
58+
```text
59+
7+2+5+3+2+2+6+2+5+3+1+2+0+5+3+9 = 57
60+
```
61+
62+
57 is not evenly divisible by 10, so this number is not valid.
63+
64+
[luhn]: https://en.wikipedia.org/wiki/Luhn_algorithm
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"authors": [
3+
"SimaDovakin"
4+
],
5+
"files": {
6+
"solution": [
7+
"luhn.u"
8+
],
9+
"test": [
10+
"luhn.test.u"
11+
],
12+
"example": [
13+
".meta/examples/luhn.example.u"
14+
]
15+
},
16+
"blurb": "Given a number determine whether or not it is valid per the Luhn formula.",
17+
"source": "The Luhn Algorithm on Wikipedia",
18+
"source_url": "https://en.wikipedia.org/wiki/Luhn_algorithm"
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
luhn.isValid : Text -> Boolean
2+
luhn.isValid string =
3+
charList = toCharList string
4+
digitCharList = List.filter (Class.is Class.digit) charList
5+
match (charList |> any (not << isValidChar), 1 >= size digitCharList) with
6+
(false, false) -> digitCharList
7+
|> map toDigit
8+
|> checkSum
9+
|> (s -> 0 == mod s 10)
10+
(_, _) -> false
11+
12+
luhn.checkSum : [Optional Nat] -> Nat
13+
luhn.checkSum digits =
14+
digits
15+
|> foldRight accumulateDigits (0, 0)
16+
|> at2
17+
18+
luhn.isValidChar : Char -> Boolean
19+
luhn.isValidChar char =
20+
charClass = Class.or Class.digit Class.whitespace
21+
Class.is charClass char
22+
23+
luhn.toDigit : Char -> Optional Nat
24+
luhn.toDigit char =
25+
codePoint = toNat char
26+
match inRange 48 58 codePoint with
27+
true -> Some (codePoint - 48)
28+
false -> None
29+
30+
luhn.doubleDigit : Nat -> Nat
31+
luhn.doubleDigit digit =
32+
doubledDigit = digit * 2
33+
match doubledDigit > 9 with
34+
true -> doubledDigit - 9
35+
false -> doubledDigit
36+
37+
luhn.accumulateDigits : Optional Nat -> (Nat, Nat) -> (Nat, Nat)
38+
luhn.accumulateDigits digit acc =
39+
(index, totalSum) = acc
40+
match (digit, 1 == mod index 2) with
41+
(Some d, true) -> (index + 1, totalSum + doubleDigit d)
42+
(Some d, false) -> (index + 1, totalSum + d)
43+
(_, _) -> (index, totalSum)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
[
2+
{
3+
"name": "luhn.isValid.tests.ex1",
4+
"test_code": "expect (false == luhn.isValid \"1\")\n |> Test.label \"single digit strings can not be valid\""
5+
},
6+
{
7+
"name": "luhn.isValid.tests.ex2",
8+
"test_code": "expect (false == luhn.isValid \"0\")\n |> Test.label \"a single zero is invalid\""
9+
},
10+
{
11+
"name": "luhn.isValid.tests.ex3",
12+
"test_code": "expect (true == luhn.isValid \"059\")\n |> Test.label \"a simple valid SIN that remains valid if reversed\""
13+
},
14+
{
15+
"name": "luhn.isValid.tests.ex4",
16+
"test_code": "expect (true == luhn.isValid \"59\")\n |> Test.label \"a simple valid SIN that becomes invalid if reversed\""
17+
},
18+
{
19+
"name": "luhn.isValid.tests.ex5",
20+
"test_code": "expect (true == luhn.isValid \"055 444 285\")\n |> Test.label \"a valid Canadian SIN\""
21+
},
22+
{
23+
"name": "luhn.isValid.tests.ex6",
24+
"test_code": "expect (false == luhn.isValid \"055 444 286\")\n |> Test.label \"invalid Canadian SIN\""
25+
},
26+
{
27+
"name": "luhn.isValid.tests.ex7",
28+
"test_code": "expect (false == luhn.isValid \"8273 1232 7352 0569\")\n |> Test.label \"invalid credit card\""
29+
},
30+
{
31+
"name": "luhn.isValid.tests.ex8",
32+
"test_code": "expect (false == luhn.isValid \"1 2345 6789 1234 5678 9012\")\n |> Test.label \"invalid long number with an even remainder\""
33+
},
34+
{
35+
"name": "luhn.isValid.tests.ex9",
36+
"test_code": "expect (false == luhn.isValid \"1 2345 6789 1234 5678 9013\")\n |> Test.label \"invalid long number with a remainder divisible by 5\""
37+
},
38+
{
39+
"name": "luhn.isValid.tests.ex10",
40+
"test_code": "expect (true == luhn.isValid \"095 245 88\")\n |> Test.label \"valid number with an even number of digits\""
41+
},
42+
{
43+
"name": "luhn.isValid.tests.ex11",
44+
"test_code": "expect (true == luhn.isValid \"234 567 891 234\")\n |> Test.label \"valid number with an odd number of spaces\""
45+
},
46+
{
47+
"name": "luhn.isValid.tests.ex12",
48+
"test_code": "expect (false == luhn.isValid \"059a\")\n |> Test.label \"valid strings with a non-digit added at the end become invalid\""
49+
},
50+
{
51+
"name": "luhn.isValid.tests.ex13",
52+
"test_code": "expect (false == luhn.isValid \"055-444-285\")\n |> Test.label \"valid strings with punctuation included become invalid\""
53+
},
54+
{
55+
"name": "luhn.isValid.tests.ex14",
56+
"test_code": "expect (false == luhn.isValid \"055# 444$ 285\")\n |> Test.label \"valid strings with symbols included become invalid\""
57+
},
58+
{
59+
"name": "luhn.isValid.tests.ex15",
60+
"test_code": "expect (false == luhn.isValid \" 0\")\n |> Test.label \"single zero with space is invalid\""
61+
},
62+
{
63+
"name": "luhn.isValid.tests.ex16",
64+
"test_code": "expect (true == luhn.isValid \"0000 0\")\n |> Test.label \"more than a single zero is valid\""
65+
},
66+
{
67+
"name": "luhn.isValid.tests.ex17",
68+
"test_code": "expect (true == luhn.isValid \"091\")\n |> Test.label \"input digit 9 is correctly converted to output digit 9\""
69+
},
70+
{
71+
"name": "luhn.isValid.tests.ex18",
72+
"test_code": "expect (true == luhn.isValid \"9999999999 9999999999 9999999999 9999999999\")\n |> Test.label \"very long input is valid\""
73+
},
74+
{
75+
"name": "luhn.isValid.tests.ex19",
76+
"test_code": "expect (true == luhn.isValid \"109\")\n |> Test.label \"valid luhn with an odd number of digits and non zero first digit\""
77+
},
78+
{
79+
"name": "luhn.isValid.tests.ex20",
80+
"test_code": "expect (false == luhn.isValid \"055b 444 285\")\n |> Test.label \"using ascii value for non-doubled non-digit isn't allowed\""
81+
},
82+
{
83+
"name": "luhn.isValid.tests.ex21",
84+
"test_code": "expect (false == luhn.isValid \":9\")\n |> Test.label \"using ascii value for doubled non-digit isn't allowed\""
85+
},
86+
{
87+
"name": "luhn.isValid.tests.ex22",
88+
"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\""
89+
}
90+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Testing transcript for luhn exercise
2+
3+
```ucm
4+
.> load ./luhn.u
5+
.> add
6+
.> load ./luhn.test.u
7+
.> add
8+
.> move.term luhn.tests tests
9+
```
+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# This is an auto-generated file.
2+
#
3+
# Regenerating this file via `configlet sync` will:
4+
# - Recreate every `description` key/value pair
5+
# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications
6+
# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion)
7+
# - Preserve any other key/value pair
8+
#
9+
# As user-added comments (using the # character) will be removed when this file
10+
# is regenerated, comments can be added via a `comment` key.
11+
12+
[792a7082-feb7-48c7-b88b-bbfec160865e]
13+
description = "single digit strings can not be valid"
14+
15+
[698a7924-64d4-4d89-8daa-32e1aadc271e]
16+
description = "a single zero is invalid"
17+
18+
[73c2f62b-9b10-4c9f-9a04-83cee7367965]
19+
description = "a simple valid SIN that remains valid if reversed"
20+
21+
[9369092e-b095-439f-948d-498bd076be11]
22+
description = "a simple valid SIN that becomes invalid if reversed"
23+
24+
[8f9f2350-1faf-4008-ba84-85cbb93ffeca]
25+
description = "a valid Canadian SIN"
26+
27+
[1cdcf269-6560-44fc-91f6-5819a7548737]
28+
description = "invalid Canadian SIN"
29+
30+
[656c48c1-34e8-4e60-9a5a-aad8a367810a]
31+
description = "invalid credit card"
32+
33+
[20e67fad-2121-43ed-99a8-14b5b856adb9]
34+
description = "invalid long number with an even remainder"
35+
36+
[7e7c9fc1-d994-457c-811e-d390d52fba5e]
37+
description = "invalid long number with a remainder divisible by 5"
38+
39+
[ad2a0c5f-84ed-4e5b-95da-6011d6f4f0aa]
40+
description = "valid number with an even number of digits"
41+
42+
[ef081c06-a41f-4761-8492-385e13c8202d]
43+
description = "valid number with an odd number of spaces"
44+
45+
[bef66f64-6100-4cbb-8f94-4c9713c5e5b2]
46+
description = "valid strings with a non-digit added at the end become invalid"
47+
48+
[2177e225-9ce7-40f6-b55d-fa420e62938e]
49+
description = "valid strings with punctuation included become invalid"
50+
51+
[ebf04f27-9698-45e1-9afe-7e0851d0fe8d]
52+
description = "valid strings with symbols included become invalid"
53+
54+
[08195c5e-ce7f-422c-a5eb-3e45fece68ba]
55+
description = "single zero with space is invalid"
56+
57+
[12e63a3c-f866-4a79-8c14-b359fc386091]
58+
description = "more than a single zero is valid"
59+
60+
[ab56fa80-5de8-4735-8a4a-14dae588663e]
61+
description = "input digit 9 is correctly converted to output digit 9"
62+
63+
[b9887ee8-8337-46c5-bc45-3bcab51bc36f]
64+
description = "very long input is valid"
65+
66+
[8a7c0e24-85ea-4154-9cf1-c2db90eabc08]
67+
description = "valid luhn with an odd number of digits and non zero first digit"
68+
69+
[39a06a5a-5bad-4e0f-b215-b042d46209b1]
70+
description = "using ascii value for non-doubled non-digit isn't allowed"
71+
72+
[f94cf191-a62f-4868-bc72-7253114aa157]
73+
description = "using ascii value for doubled non-digit isn't allowed"
74+
75+
[8b72ad26-c8be-49a2-b99c-bcc3bf631b33]
76+
description = "non-numeric, non-space char in the middle with a sum that's divisible by 10 isn't allowed"

0 commit comments

Comments
 (0)