Skip to content

Commit

Permalink
Update alphametics exercise tests to match canonical data (exercism#285)
Browse files Browse the repository at this point in the history
  • Loading branch information
ErikSchierboom authored and robkeim committed Jan 16, 2017
1 parent e3bd74f commit f6f8c31
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 85 deletions.
16 changes: 8 additions & 8 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,14 @@
"Transforming"
]
},
{
"slug": "alphametics",
"difficulty": 9,
"topics": [
"Parsing",
"Maps"
]
},
{
"slug": "zipper",
"difficulty": 10,
Expand All @@ -828,14 +836,6 @@
"Recursion",
"Searching"
]
},
{
"slug": "alphametics",
"difficulty": 10,
"topics": [
"Parsing",
"Maps"
]
}
],
"deprecated": [
Expand Down
46 changes: 33 additions & 13 deletions exercises/alphametics/AlphameticsTest.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,54 @@ open NUnit.Framework
open Alphametics

[<Test>]
let ``Can solve short puzzles`` () =
let ``Puzzle with three letters`` () =
let actual = solve "I + BB == ILL"
let expected = ['I', 1; 'B', 9; 'L', 0] |> Map.ofList |> Some
Assert.That(actual, Is.EqualTo(expected))

[<Test>]
[<Ignore("Remove to run test")>]
let ``Can solve long puzzles`` () =
let actual = solve "SEND + MORE == MONEY"
let expected = ['S', 9; 'E', 5; 'N', 6; 'D', 7; 'M', 1; 'O', 0; 'R', 8; 'Y', 2] |> Map.ofList |> Some
let ``Solution must have unique value for each letter`` () =
let actual = solve "A == B"
Assert.That(actual, Is.EqualTo(None))

[<Test>]
[<Ignore("Remove to run test")>]
let ``Leading zero solution is invalid`` () =
let actual = solve "ACA + DD == BD"
Assert.That(actual, Is.EqualTo(None))

[<Test>]
[<Ignore("Remove to run test")>]
let ``Puzzle with four letters`` () =
let actual = solve "AS + A == MOM"
let expected = ['A', 9; 'S', 2; 'M', 1; 'O', 0] |> Map.ofList |> Some
Assert.That(actual, Is.EqualTo(expected))

[<Test>]
[<Ignore("Remove to run test")>]
let ``Can solve puzzles with multiplication`` () =
let actual = solve "IF * DR == DORI"
let expected = ['I', 8; 'F', 2; 'D', 3; 'R', 9; 'O', 1] |> Map.ofList |> Some
let ``Puzzle with six letters`` () =
let actual = solve "NO + NO + TOO == LATE"
let expected = ['N', 7; 'O', 4; 'T', 9; 'L', 1; 'A', 0; 'E', 2] |> Map.ofList |> Some
Assert.That(actual, Is.EqualTo(expected))

[<Test>]
[<Ignore("Remove to run test")>]
let ``Can solve puzzles with any boolean expression`` () =
let actual = solve "PI * R ^ 2 == AREA"
let expected = ['P', 9; 'I', 6; 'R', 7; 'A', 4; 'E', 0] |> Map.ofList |> Some
let ``Puzzle with seven letters`` () =
let actual = solve "HE + SEES + THE == LIGHT"
let expected = ['E', 4; 'G', 2; 'H', 5; 'I', 0; 'L', 1; 'S', 9; 'T', 7] |> Map.ofList |> Some
Assert.That(actual, Is.EqualTo(expected))

[<Test>]
[<Ignore("Remove to run test")>]
let ``Puzzle with eight letters`` () =
let actual = solve "SEND + MORE == MONEY"
let expected = ['S', 9; 'E', 5; 'N', 6; 'D', 7; 'M', 1; 'O', 0; 'R', 8; 'Y', 2] |> Map.ofList |> Some
Assert.That(actual, Is.EqualTo(expected))

[<Test>]
[<Ignore("Remove to run test")>]
let ``Cannot solve unsolvable puzzles`` () =
let actual = solve "A * B == A + B"
Assert.That(actual, Is.EqualTo(None))
let ``Puzzle with ten letters`` () =
let actual = solve "AND + A + STRONG + OFFENSE + AS + A + GOOD == DEFENSE"
let expected = ['A', 5; 'D', 3; 'E', 4; 'F', 7; 'G', 8; 'N', 0; 'O', 2; 'R', 1; 'S', 6; 'T', 9] |> Map.ofList |> Some
Assert.That(actual, Is.EqualTo(expected))
93 changes: 29 additions & 64 deletions exercises/alphametics/Example.fs
Original file line number Diff line number Diff line change
@@ -1,60 +1,25 @@
module Alphametics

open System
open System.Text.RegularExpressions

type Operators = Plus | Mult | Pow | Eq

type Token =
| Operand of int
| Operator of Operators

let (|Int|_|) str =
match Int32.TryParse(str) with
| (true, i) -> Some i
| _ -> None

let token (str: string) =
match str with
| "+" -> Operator Plus
| "*" -> Operator Mult
| "^" -> Operator Pow
| "==" -> Operator Eq
| Int i -> Operand i
| _ -> failwith "Invalid token"

let breakBy item list =
match List.tryFindIndex (fun x -> x = item) list with
| None -> None
| Some i -> Some (list.[0..i - 1], list.[i + 1..])

let tokenize (str: string) =
str.Split()
|> List.ofArray
|> List.map token
|> breakBy (Operator Eq)

let operators =
[(Pow, fun x y -> pown x y |> Operand);
(Mult, fun x y -> x * y |> Operand);
(Plus, fun x y -> x + y |> Operand)]

let rec simplifyOperator (operator, op) acc remainder =
match remainder with
| Operand x::Operator y::Operand z::xs when y = operator -> simplifyOperator (operator, op) (op x z::acc) xs
| x::xs -> simplifyOperator (operator, op) (x::acc) xs
| [] -> acc |> List.rev
open FParsec
open System.Text.RegularExpressions

let simplify equation = operators |> List.fold (fun acc operator -> simplifyOperator operator [] acc) equation
let plus = pstring " + "
let equal = pstring " == "
let operand = many1Satisfy isAsciiUpper
let expression = sepBy operand plus
let equation = expression .>> equal .>>. expression

let solveEquation (left, right) = simplify left = simplify right
let parseToOption parser (input: string) =
match run parser input with
| Success(result, _, _) -> Some result
| Failure(errorMsg, _, _) -> None

let equationIsCorrect input =
match tokenize input with
| None -> false
| Some equation -> solveEquation equation
let parseEquation = parseToOption equation

let chars (str: string) = str |> Seq.filter (Char.IsLetter) |> Set.ofSeq
let operandToInt (map: Map<char, int>) (operand: string) =
Seq.fold (fun acc x -> acc * 10 + Map.find x map) 0 operand

let generateCombinations length =
let rec helper remaining options =
Expand All @@ -65,23 +30,23 @@ let generateCombinations length =

helper length ([0..9] |> Set.ofList)

let generateMaps (str: string) =
let c = chars str
let nonZeroChars =
Regex.Matches (str, "([A-Z])[A-Z]*")
|> Seq.cast<Match>
|> Seq.map (fun m -> m.Groups |> Seq.cast<Group> |> Seq.map (fun n -> n.Value.Chars 0) |> Seq.item 1)
|> Set.ofSeq
let generateMaps (leftOperands, rightOperands) =
let operands = leftOperands @ rightOperands
let chars = operands |> String.concat "" |> Set.ofSeq
let nonZeroChars = operands |> List.map Seq.head |> Set.ofList

generateCombinations (Set.count c)
|> Seq.map (Seq.zip c >> Map.ofSeq)
generateCombinations (Set.count chars)
|> Seq.map (Seq.zip chars >> Map.ofSeq)
|> Seq.filter (fun m -> Set.forall (fun x -> Map.find x m <> 0) nonZeroChars)

let trySolve (input: string) map =
map
|> Map.fold (fun (acc: string) (key: char) (value: int) -> acc.Replace(string key, string value)) input
|> equationIsCorrect
let sumOperands lettersToDigits = List.sumBy (operandToInt lettersToDigits)

let trySolve (leftOperands, rightOperands) lettersToDigits =
let left = sumOperands lettersToDigits leftOperands
let right = sumOperands lettersToDigits rightOperands
left = right

let solve input =
generateMaps input
|> Seq.tryFind (trySolve input)
match parseEquation input with
| Some equation -> Seq.tryFind (trySolve equation) (generateMaps equation)
| None -> None

0 comments on commit f6f8c31

Please sign in to comment.