Skip to content

Commit

Permalink
Fix algorithm and tests for Strings/MinCostStringConversion (#29)
Browse files Browse the repository at this point in the history
* Uncomment tests

* Fix algorithm

* Add new tests and attempt to fix old tests

* fix computeTransformTables tests

* remove trailing spaces

* remove trailing spaces

* Remove option type from internal implementation
  • Loading branch information
mahdihasnat authored Nov 23, 2024
1 parent d8ffc18 commit d0c57ac
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 74 deletions.
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

0 comments on commit d0c57ac

Please sign in to comment.