Skip to content

Fix algorithm and tests for Strings/MinCostStringConversion #29

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Nov 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 95 additions & 17 deletions Algorithms.Tests/Strings/MinCostStringConversionTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,100 @@

open Microsoft.VisualStudio.TestTools.UnitTesting
open Algorithms.Strings
open MinCostStringConversion

[<TestClass>]
type MinCostStringConversionTests () =

let validateAndApply (source: string ) (operations: Operation array) : string =
operations
|> Array.mapFold (fun sourcePosition op ->
match op with
| Operation.Copy s ->
Assert.AreEqual(source.[sourcePosition], s)
Some s, sourcePosition + 1
| Operation.Replace (s, d) ->
Assert.AreEqual(source.[sourcePosition], s)
Some d, sourcePosition + 1
| Operation.Delete s ->
Assert.AreEqual(source.[sourcePosition], s)
None, sourcePosition + 1
| Operation.Insert c ->
Some c, sourcePosition
) 0
|> fst
|> Array.choose id
|> Array.map string
|> String.concat ""

let calculateCost (operations: Operation array, copyCost:int, replaceCost:int, deleteCost:int, insertCost:int) =
operations
|> Array.sumBy (function
| Operation.Copy _ -> copyCost
| Operation.Replace _ -> replaceCost
| Operation.Delete _ -> deleteCost
| Operation.Insert _ -> insertCost
)


[<TestMethod>]
[<DataRow("", "", 1, 2, 3, 4)>]
[<DataRow("github", "", 1, 2, 3, 4)>]
[<DataRow("", "github", 1, 2, 3, 4)>]
[<DataRow("github", "github", 1, 2, 3, 4)>]
[<DataRow("banana", "apple", 1, 2, 3, 4)>]
[<DataRow("banana", "apple", 3, 1, 2, 4)>]
[<DataRow("banana", "apple", 3, 1, 2, 4)>]
member this.validateResult (source: string, destination: string, copyCost:int, replaceCost:int, deleteCost:int, insertCost:int) =
let costs, ops = computeTransformTables (source, destination, copyCost, replaceCost, deleteCost, insertCost)

for i = 0 to source.Length do
for j = 0 to destination.Length do
let sourceSubstring = source.Substring(0, i)
let destinationSubstring = destination.Substring(0, j)
let operations = assembleTransformation (ops, i, j)
let actualDestinationSubstring = validateAndApply sourceSubstring operations
let calculatedCost = calculateCost (operations, copyCost, replaceCost, deleteCost, insertCost)
Assert.AreEqual (destinationSubstring, actualDestinationSubstring)
Assert.AreEqual (costs.[i].[j], calculatedCost)

static member inputForComputeTransformTables =
seq {
yield [|
"abbbaba" :> obj
"ababa" :> obj
1 :> obj
2 :> obj
3 :> obj
3 :> obj
([|
[|0; 3; 6; 9; 12; 15|]
[|3; 1; 4; 7; 10; 13|]
[|6; 4; 2; 5; 8; 11|]
[|9; 7; 5; 4; 6; 9|]
[|12; 10; 8; 7; 5; 8|]
[|15; 13; 11; 9; 8; 6|]
[|18; 16; 14; 12; 10; 9|]
[|21; 19; 17; 15; 13; 11|]
|],
[|
[|Operation.Copy 'a'; Operation.Insert 'a'; Operation.Insert 'b'; Operation.Insert 'a'; Operation.Insert 'b'; Operation.Insert 'a'|]
[|Operation.Delete 'a'; Operation.Copy 'a'; Operation.Insert 'b'; Operation.Copy 'a'; Operation.Insert 'b'; Operation.Copy 'a'|]
[|Operation.Delete 'b'; Operation.Delete 'b'; Operation.Copy 'b'; Operation.Insert 'a'; Operation.Copy 'b'; Operation.Insert 'a'|]
[|Operation.Delete 'b'; Operation.Delete 'b'; Operation.Copy 'b'; Operation.Replace ('b', 'a'); Operation.Copy 'b'; Operation.Insert 'a'|]
[|Operation.Delete 'b'; Operation.Delete 'b'; Operation.Copy 'b'; Operation.Replace ('b', 'a'); Operation.Copy 'b'; Operation.Replace ('b', 'a')|]
[|Operation.Delete 'a'; Operation.Copy 'a'; Operation.Delete 'a'; Operation.Copy 'a'; Operation.Delete 'a'; Operation.Copy 'a'|]
[|Operation.Delete 'b'; Operation.Delete 'b'; Operation.Copy 'b'; Operation.Delete 'b'; Operation.Copy 'b'; Operation.Delete 'b'|]
[|Operation.Delete 'a'; Operation.Copy 'a'; Operation.Delete 'a'; Operation.Copy 'a'; Operation.Delete 'a'; Operation.Copy 'a'|]
|]) :> obj
|]
}

[<TestMethod>]
[<DynamicData(nameof(MinCostStringConversionTests.inputForComputeTransformTables))>]
member this.computeTransformTables (sourceString:string, destinationString:string, copyCost:int, replaceCost:int, deleteCost:int, insertCost:int, expected:int array array * Operation array array) =
let actual = MinCostStringConversion.computeTransformTables(sourceString,destinationString,copyCost,replaceCost,deleteCost,insertCost)
Assert.IsTrue((expected = actual))


// FIXME
// [<TestClass>]
// type MinCostStringConversionTests () =

// [<TestMethod>]
// [<DataRow("abbbaba", "abbba")>]
// [<DataRow("ababa", "ababa")>]
// member this.assembleTransformation (ops:string list, i:int, j:int, expected:string list) =
// let actual = MinCostStringConversion.assembleTransformation(ops, i, j)
// Assert.AreEqual(expected, actual)

// [<TestMethod>]
// [<DataRow("abbbaba", "abbba")>]
// [<DataRow("ababa", "ababa")>]
// member this.assembleTransformation (sourceString:string, destinationString:string, copyCost:int, replaceCost:int, deleteCost:int, insertCost:int, expected:int list * string list) =
// let actual = MinCostStringConversion.computeTransformTables(sourceString,destinationString,copyCost,replaceCost,deleteCost,insertCost)
// Assert.AreEqual(expected, actual)

95 changes: 38 additions & 57 deletions Algorithms/Strings/MinCostStringConversion.fs
Original file line number Diff line number Diff line change
Expand Up @@ -9,90 +9,71 @@
namespace Algorithms.Strings

module MinCostStringConversion =

[<RequireQualifiedAccess>]
type Operation =
| Copy of char
| Replace of Source: char * Target: char
| Delete of char
| Insert of char

let computeTransformTables
(
sourceString: string,
destinationString: string,
source: string,
destination: string,
copyCost: int,
replaceCost: int,
deleteCost: int,
insertCost: int
): list<int> * list<string> =
let sourceSeq = [ sourceString ]
let destinationSeq = [ destinationString ]
let lenSourceSeq = sourceSeq.Length
let lenDestinationSeq = destinationSeq.Length
): array<array<int>> * array<array<Operation>> =

let costs =
[| for i in 0 .. (lenSourceSeq + 1) -> [| for i in 0 .. lenDestinationSeq + 1 -> 0 |] |]
Array.init (source.Length + 1) (fun _ -> Array.init (destination.Length + 1) (fun _ -> 0))

let ops =
[| for i in 0 .. lenSourceSeq + 1 -> [| for i in 0 .. lenDestinationSeq + 1 -> "" |] |]
Array.init (source.Length + 1) (fun _ -> Array.init (destination.Length + 1) (fun _ -> Operation.Copy 'a'))

for i = 1 to lenSourceSeq + 1 do
for i = 1 to source.Length do
costs.[i].[0] <- i * deleteCost
ops.[i].[0] <- sprintf "D%s" (sourceSeq.[i - 1])
ops.[i].[0] <- Operation.Delete source.[i - 1]

for i = 1 to lenDestinationSeq + 1 do
for i = 1 to destination.Length do
costs.[0].[i] <- i * insertCost
ops.[0].[i] <- sprintf "I%s" (destinationSeq.[i - 1])
ops.[0].[i] <- Operation.Insert destination.[i - 1]

for i in 1 .. lenSourceSeq + 1 do
for j in 1 .. lenDestinationSeq + 1 do
if sourceSeq.[i - 1] = destinationSeq.[j - 1] then
for i in 1 .. source.Length do
for j in 1 .. destination.Length do
if source.[i - 1] = destination.[j - 1] then
costs.[i].[j] <- costs.[i - 1].[j - 1] + copyCost
ops.[i].[j] <- sprintf "C%s" (sourceSeq.[i - 1])
ops.[i].[j] <- Operation.Copy (source.[i - 1])
else
costs.[i].[j] <- costs.[i - 1].[j - 1] + replaceCost

ops.[i].[j] <-
sprintf
"R%s"
(sourceSeq.[i - 1]
+ (string) (destinationSeq.[j - 1]))
ops.[i].[j] <- Operation.Replace (source.[i - 1], destination.[j - 1])

if costs.[i - 1].[j] + deleteCost < costs.[i].[j] then
costs.[i].[j] <- costs.[i - 1].[j] + deleteCost
ops.[i].[j] <- sprintf "D%s" (sourceSeq.[i - 1])
ops.[i].[j] <- Operation.Delete (source.[i - 1])

if costs.[i].[j - 1] + insertCost < costs.[i].[j] then
costs.[i].[j] <- costs.[i].[j - 1] + insertCost
ops.[i].[j] <- sprintf "I%s" (destinationSeq.[j - 1])
ops.[i].[j] <- Operation.Insert destination.[j - 1]

costs |> Seq.cast<int> |> Seq.toList, ops |> Seq.cast<string> |> Seq.toList
costs, ops

let rec assembleTransformation (ops: list<string>, i: int, j: int): list<string> =
let rec assembleTransformation (ops: array<array<Operation>>, i: int, j: int): array<Operation> =
printfn $"i={i},j={j},%A{ops}"
if i = 0 && j = 0 then
List.empty
Array.empty
else
match ops.[i].[j] with
| o when o = 'C' || o = 'R' ->
let mutable seq =
assembleTransformation (ops, i - 1, j - 1)
|> List.toArray

let ch =
[ ((string) ops.[i].[j]) ] |> List.toArray

seq <- seq |> Array.append ch
seq |> List.ofArray
| 'D' ->
let mutable seq =
assembleTransformation (ops, i - 1, j)
|> List.toArray

let ch =
[ ((string) ops.[i].[j]) ] |> List.toArray

seq <- seq |> Array.append ch
seq |> List.ofArray
| _ ->
let mutable seq =
assembleTransformation (ops, i, j - 1)
|> List.toArray

let ch =
[ ((string) ops.[i].[j]) ] |> List.toArray
| Operation.Replace _
| Operation.Copy _ ->
let seq = assembleTransformation (ops, i - 1, j - 1)
Array.append seq [| ops[i][j] |]
| Operation.Delete _ ->
let seq = assembleTransformation (ops, i - 1, j)
Array.append seq [| ops[i][j] |]
| Operation.Insert _ ->
let seq = assembleTransformation (ops, i , j - 1)
Array.append seq [| ops[i][j] |]

seq <- seq |> Array.append ch
seq |> List.ofArray
Loading