I'm working on a function that reduces pairs of elements if they meet some criteria.
For example, given an array: ["1", "2", "a", "b", "3", "4"]
I want to transform it so that strings that represent numbers are combined with addition, and strings that represent letters are concatenated so the output would be: ["3" "ab" "7"]
.
The approach I've taken is to sort of gobble the array up from the head pair, which with the example above goes through the following steps:
(1, 2, a, b, 3, 4) // combine 1 and 2 ->
(3, a, b, 3, 4) // combine a and b ->
(3, ab, 3, 4) // combine 3 and 4 ->
(3, ab, 7)
I'm calling transform a 'knot' because I'm visualizing elements in the array being knotted together to unlimitedly shorten the sequence.
Is this a pattern that's known by some other name? Any suggestions other than knot that might be more descriptive (knit? squish?).
Here's the current approach in Swift, would be great to get any feedback on style, documentation, or algorithm here:
public extension Array {
/// A `knot` is like a discriminating `reduce`.
///
/// Knotting an array incrementally looks at the next pair of sequential elements, and attempts to use
/// a closure to reduce that pair of elements into a single element.
///
/// Unlike `reduce` this closure can be failable, in which case the element is not reduced, but simply
/// appended to the results.
///
/// - Note: Forward knots.
/// If (A B C D) can be knotted successfully as (ABCD) through series of knots:
/// (A B C D) -> (AB C D) -> (ABC D) -> (ABCD).
///
/// It's possible that A and B **cannot** be knotted, but A and BC **can** be knotted to achieve the
/// equivalent result:
/// (A B C D) -> (A BC D) -> (ABC D) -> ABCD
///
/// - Parameter forwardKnot: A flag to indicate if a forward-knot should be attempted if the initial knot
/// fails.
///
/// - Parameter transform: A knotting closure. `transform` accepts a pair of elements from this sequence
/// as its parameter and returns a transformed value of the same type if the pair should be combined, or
/// nil if the element can't be combined.
///
/// - Returns: An array containing the transformed elements of this sequence.
func knot(forwards forwardKnot: Bool = false,
_ transform: ((Element, Element) -> Element?)) -> [Element] {
var upcoming = self
var knotted: [Element] = []
knotted.reserveCapacity(count)
while !upcoming.isEmpty {
let next = upcoming.remove(at: 0)
// first iteration - nothing to knot, just add next element
guard let last = knotted.last else {
knotted.append(next)
continue
}
// attempt to knot
if let knot = transform(last, next) {
knotted[knotted.count-1] = knot
}
else {
if forwardKnot, let ahead = upcoming.first,
let under = transform(next, ahead),
let over = transform(last, under) {
upcoming.remove(at: 0)
knotted[knotted.count-1] = over
}
// can't forward knot, just add the next element
else {
knotted.append(next)
}
}
}
return knotted
}
}
Example usage:
func additiveKnot(left: String, right: String) -> String? {
let left: Any = Int(left) ?? left
let right: Any = Int(right) ?? right
switch (left, right) {
case let(left as String, right as String):
// knot Strings by concatenating
// "a", "b" => "ab"
return left + right
case let (left as Int, right as Int):
// knot Ints by addition
// 1, 2 => 3
return String(left + right)
default:
return nil
}
}
let values = ["1", "2", "a", "b", "3", "4"]
let knotted = values.knot(additiveKnot)
// 'knotted' == ["3", "ab", "7"]