diff --git a/Sources/LevenshteinTransformations/Extensions/Array.swift b/Sources/LevenshteinTransformations/Extensions/Array.swift index ce79f29..6d80b0c 100644 --- a/Sources/LevenshteinTransformations/Extensions/Array.swift +++ b/Sources/LevenshteinTransformations/Extensions/Array.swift @@ -11,14 +11,14 @@ extension Array where Element: Equatable { /// - Parameter target: The target array. /// - Returns: The Levenshtein distance. public func levenshteinDistance(to target: [Element]) -> Int { - getTransformations(to: target).count + levenshteinDistance(to: target, id: \.self) } /// Get the transformations needed to transform the array into the target array. /// - Parameter target: The target array. /// - Returns: The transformations. public func getTransformations(to target: [Element]) -> [Transformation] { - LevenshteinTransformations.levenshteinTransformations(from: self, to: target) + getTransformations(to: target, id: \.self) } /// Call every transformation step needed to transform the array into the target array. @@ -26,12 +26,7 @@ extension Array where Element: Equatable { /// - target: The target array. /// - functions: The transformation functions. public func transform(to target: [Element], functions: Functions) { - var transformations = getTransformations(to: target) - - while !transformations.isEmpty { - let transformation = transformations.removeFirst() - transformation.transform(functions: functions, nextTransformations: &transformations) - } + transform(to: target, id: \.self, functions: functions) } /// Call every transformation step needed to transform the array into the target array. @@ -39,12 +34,7 @@ extension Array where Element: Equatable { /// - target: The target array. /// - functions: The transformation functions. public func transform(to target: [Element], functions: AsyncFunctions) async { - var transformations = getTransformations(to: target) - - while !transformations.isEmpty { - let transformation = transformations.removeFirst() - await transformation.transform(functions: functions, nextTransformations: &transformations) - } + await transform(to: target, id: \.self, functions: functions) } } @@ -70,7 +60,71 @@ extension Array where Element: Identifiable { /// - target: The target array. /// - functions: The transformation functions. public func identifiableTransform(to target: [Element], functions: Functions) { - var transformations = identifiableGetTransformations(to: target) + transform(to: target, id: \.id, functions: functions) + } + + /// Call every transformation step needed to transform the array into the target array. + /// - Parameters: + /// - target: The target array. + /// - functions: The transformation functions. + public func identifiableTransform(to target: [Element], functions: AsyncFunctions) async { + await transform(to: target, id: \.id, functions: functions) + } + +} + +extension Array { + + /// Calculate the Levenshtein distance to another array. + /// - Parameters: + /// - target: The target array. + /// - id: The identifier. + /// - Returns: The Levenshtein distance. + public func levenshteinDistance( + to target: [Element], + id: KeyPath + ) -> Int where Identifier: Equatable { + getRawTransformations(to: target, id: id).count + } + + /// Get the transformations needed to transform the array into the target array with the raw identifier data. + /// - Parameters: + /// - target: The target array. + /// - id: The identifier. + /// - Returns: The transformations applied to the identifier. + public func getRawTransformations( + to target: [Element], + id: KeyPath + ) -> [Transformation] where Identifier: Equatable { + let equatableSource = map { $0[keyPath: id] } + let equatableTarget = target.map { $0[keyPath: id] } + return LevenshteinTransformations.levenshteinTransformations(from: equatableSource, to: equatableTarget) + } + + /// Get the transformations needed to transform the array into the target array. + /// - Parameters: + /// - target: The target array. + /// - id: The identifier. + /// - Returns: The transformations. + public func getTransformations( + to target: [Element], + id: KeyPath + ) -> [Transformation] where Identifier: Equatable { + getRawTransformations(to: target, id: id) + .compactMap { $0.convert { targetID in target.first { $0[keyPath: id] == targetID } } } + } + + /// Call every transformation step needed to transform the array into the target array. + /// - Parameters: + /// - target: The target array. + /// - id: The identifier key path. + /// - functions: The transformation functions. + public func transform( + to target: [Element], + id: KeyPath, + functions: Functions + ) where Identifier: Equatable { + var transformations = getTransformations(to: target, id: id) while !transformations.isEmpty { let transformation = transformations.removeFirst() @@ -81,9 +135,14 @@ extension Array where Element: Identifiable { /// Call every transformation step needed to transform the array into the target array. /// - Parameters: /// - target: The target array. + /// - id: The identifier key path. /// - functions: The transformation functions. - public func identifiableTransform(to target: [Element], functions: AsyncFunctions) async { - var transformations = identifiableGetTransformations(to: target) + public func transform( + to target: [Element], + id: KeyPath, + functions: AsyncFunctions + ) async where Identifier: Equatable { + var transformations = getTransformations(to: target, id: id) while !transformations.isEmpty { let transformation = transformations.removeFirst() diff --git a/Sources/LevenshteinTransformations/LevenshteinTransformations.docc/GettingStarted.md b/Sources/LevenshteinTransformations/LevenshteinTransformations.docc/GettingStarted.md index 36a4b3b..0c73e33 100644 --- a/Sources/LevenshteinTransformations/LevenshteinTransformations.docc/GettingStarted.md +++ b/Sources/LevenshteinTransformations/LevenshteinTransformations.docc/GettingStarted.md @@ -20,17 +20,17 @@ let target = [0, 1, 5, 6, 10] There are three functions available for arrays containing elements conforming to `Equatable` and strings. -### Arrays with Equatable Elements and Strings +### Arrays and Strings #### Levenshtein Distance -Get the Levenshtein distance with ``Swift/Array/levenshteinDistance(to:)`` (for strings: ``Swift/String/levenshteinDistance(to:)``). +Get the Levenshtein distance with ``Swift/Array/levenshteinDistance(to:id:)`` (for strings: ``Swift/String/levenshteinDistance(to:)``). ```swift print(source.levenshteinDistance(to: target)) ``` This example outputs `3`. #### Transformation Steps -Get the individual transformation steps with ``Swift/Array/getTransformations(to:)`` (for strings: ``Swift/String/getTransformations(to:)``). +Get the individual transformation steps with ``Swift/Array/getTransformations(to:id:)`` (for strings: ``Swift/String/getTransformations(to:)``). ```swift print(source.getTransformations(to: target).map { $0.description(source: source) }.joined(separator: "\n")) ``` @@ -42,7 +42,7 @@ Insert '10' at position 4 ``` #### Run Functions for the Steps -Directly run functions for the `replace`, `delete` and `insert` transformations using the ``Swift/Array/transform(to:functions:)-2wzm8`` (for strings: ``Swift/String/transform(to:functions:)-3b6nc``) function. +Directly run functions for the `replace`, `delete` and `insert` transformations using the ``Swift/Array/transform(to:id:functions:)-2wzm8`` (for strings: ``Swift/String/transform(to:id:functions:)-3b6nc``) function. ```swift var modified = source @@ -61,7 +61,7 @@ print(modified == target) ``` This example outputs `true`. -The transform functions are available in an asynchronous version: ``Swift/Array/transform(to:functions:)-756uw`` for arrays and ``Swift/String/transform(to:functions:)-46is`` for strings. +The transform functions are available in an asynchronous version: ``Swift/Array/transform(to:id:functions:)-756uw`` for arrays and ``Swift/String/transform(to:functions:)-46is`` for strings. ### Arrays with Identifiable Elements diff --git a/Sources/LevenshteinTransformations/Transformation.swift b/Sources/LevenshteinTransformations/Transformation.swift index 1f9fda2..ad3ceab 100644 --- a/Sources/LevenshteinTransformations/Transformation.swift +++ b/Sources/LevenshteinTransformations/Transformation.swift @@ -61,6 +61,29 @@ public enum Transformation { } } + /// Get the replace transformation for an element, or nil. + /// - Parameters: + /// - index: The index to replace. + /// - element: The new element. + public static func replace(at index: Int, with element: Element?) -> Self? { + guard let element else { + return nil + } + return .replace(at: index, with: element) + } + + /// Get the insert transformation for an element, or nil. + /// - Parameters: + /// - index: The index to insert at. + /// - element. The new element. + /// - Returns: The transformation. + public static func insert(at index: Int, element: Element?) -> Self? { + guard let element else { + return nil + } + return .insert(at: index, element: element) + } + /// A description of the transformation. /// - Parameter source: The initial array. /// - Returns: The description. @@ -126,6 +149,21 @@ public enum Transformation { } } + /// Convert a series of transformations in order to be able + /// to apply the same transformation on different Swift types. + /// - Parameter convert: The conversion function. + /// - Returns: The transformations. + public func convert(convert: (Element) -> NewType?) -> Transformation? { + switch self { + case let .replace(index, element): + return .replace(at: index, with: convert(element)) + case let .delete(index): + return .delete(at: index) + case let .insert(index, element): + return .insert(at: index, element: convert(element)) + } + } + } // swiftlint:enable identifier_name diff --git a/Tests/LevenshteinTransformationsTests.swift b/Tests/LevenshteinTransformationsTests.swift index ff70abc..2672d22 100644 --- a/Tests/LevenshteinTransformationsTests.swift +++ b/Tests/LevenshteinTransformationsTests.swift @@ -21,6 +21,30 @@ enum LevenshteinTransformationsTests { } + /// Test the transform function with a custom path. + static func testCustom() { + let source = [1, 2, 5, 6].map { TestType(id: $0) } + let target = [0, 1, 5, 6, 10].map { TestType(id: $0) } + + var modified = source + + source.transform( + to: target, + id: \.id, + functions: .init { index, element in + modified[index] = element + } delete: { index in + modified.remove(at: index) + } insert: { index, element in + modified.insert(element, at: index) + } + ) + + print(source.getRawTransformations(to: target, id: \.id)) + + print(modified == target) + } + /// Test the `transform` function on an equatable array. static func testEquatable() { let source = [1, 2, 5, 6] @@ -86,6 +110,7 @@ enum LevenshteinTransformationsTests { /// Run the tests. static func main() { + testCustom() testEquatable() testIdentifiable() testString()