Skip to content

Commit

Permalink
[FS-1042] Implement transpose on Seq, List and Array (#4020)
Browse files Browse the repository at this point in the history
* Implement transpose on Seq, List and Array

* Fix transpose list of empty lists

* Update signatures of List.transpose and Array.transpose

* Avoid clone when Array.transpose called with an array

* Update implentation of Seq.transpose
  • Loading branch information
PatrickMcDonald authored and KevinRansom committed Dec 18, 2017
1 parent 4054701 commit a2c1bc0
Show file tree
Hide file tree
Showing 13 changed files with 212 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -921,6 +921,31 @@ type ArrayModule2() =

()

[<Test>]
member this.Transpose() =
// integer array
Assert.AreEqual([|[|1;4|]; [|2;5|]; [|3;6|]|], Array.transpose (seq [[|1..3|]; [|4..6|]]))
Assert.AreEqual([|[|1|]; [|2|]; [|3|]|], Array.transpose [|[|1..3|]|])
Assert.AreEqual([|[|1..2|]|], Array.transpose [|[|1|]; [|2|]|])

// string array
Assert.AreEqual([|[|"a";"d"|]; [|"b";"e"|]; [|"c";"f"|]|], Array.transpose (seq [[|"a";"b";"c"|]; [|"d";"e";"f"|]]))

// empty array
Assert.AreEqual([| |], Array.transpose [| |])

// array of empty arrays - m x 0 array transposes to 0 x m (i.e. empty)
Assert.AreEqual([| |], Array.transpose [| [||] |])
Assert.AreEqual([| |], Array.transpose [| [||]; [||] |])

// null array
let nullArr = null: string[][]
CheckThrowsArgumentNullException (fun () -> nullArr |> Array.transpose |> ignore)

// jagged arrays
CheckThrowsArgumentException (fun () -> Array.transpose [| [|1; 2|]; [|3|] |] |> ignore)
CheckThrowsArgumentException (fun () -> Array.transpose [| [|1|]; [|2; 3|] |] |> ignore)

[<Test>]
member this.Truncate() =
// integer array
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,27 @@ type ListModule02() =

()

[<Test>]
member this.Transpose() =
// integer list
Assert.AreEqual([[1; 4]; [2; 5]; [3; 6]], List.transpose (seq [[1..3]; [4..6]]))
Assert.AreEqual([[1]; [2]; [3]], List.transpose [[1..3]])
Assert.AreEqual([[1..2]], List.transpose [[1]; [2]])

// string list
Assert.AreEqual([["a";"d"]; ["b";"e"]; ["c";"f"]], List.transpose (seq [["a";"b";"c"]; ["d";"e";"f"]]))

// empty list
Assert.AreEqual([], List.transpose [])

// list of empty lists - m x 0 list transposes to 0 x m (i.e. empty)
Assert.AreEqual([], List.transpose [[]])
Assert.AreEqual([], List.transpose [[]; []])

// jagged lists
CheckThrowsArgumentException (fun () -> List.transpose [[1; 2]; [3]] |> ignore)
CheckThrowsArgumentException (fun () -> List.transpose [[1]; [2; 3]] |> ignore)

[<Test>]
member this.Truncate() =
// integer list
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1522,7 +1522,32 @@ type SeqModule2() =
// null Seq
CheckThrowsArgumentNullException(fun() -> Seq.toList null |> ignore)
()


[<Test>]
member this.Transpose() =
// integer seq
VerifySeqsEqual [seq [1; 4]; seq [2; 5]; seq [3; 6]] <| Seq.transpose (seq [seq {1..3}; seq {4..6}])
VerifySeqsEqual [seq [1]; seq [2]; seq [3]] <| Seq.transpose (seq [seq {1..3}])
VerifySeqsEqual [seq {1..2}] <| Seq.transpose (seq [seq [1]; seq [2]])

// string seq
VerifySeqsEqual [seq ["a";"d"]; seq ["b";"e"]; seq ["c";"f"]] <| Seq.transpose (seq [seq ["a";"b";"c"]; seq ["d";"e";"f"]])

// empty seq
VerifySeqsEqual Seq.empty <| Seq.transpose Seq.empty

// seq of empty seqs - m x 0 seq transposes to 0 x m (i.e. empty)
VerifySeqsEqual Seq.empty <| Seq.transpose (seq [Seq.empty])
VerifySeqsEqual Seq.empty <| Seq.transpose (seq [Seq.empty; Seq.empty])

// null seq
let nullSeq = null : seq<seq<string>>
CheckThrowsArgumentNullException (fun () -> Seq.transpose nullSeq |> ignore)

// sequences of lists
VerifySeqsEqual [seq ["a";"c"]; seq ["b";"d"]] <| Seq.transpose [["a";"b"]; ["c";"d"]]
VerifySeqsEqual [seq ["a";"c"]; seq ["b";"d"]] <| Seq.transpose (seq { yield ["a";"b"]; yield ["c";"d"] })

[<Test>]
member this.Truncate() =
// integer Seq
Expand Down
3 changes: 3 additions & 0 deletions src/fsharp/FSharp.Core.Unittests/SurfaceArea.coreclr.fs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ Microsoft.FSharp.Collections.ArrayModule: T[] Where[T](Microsoft.FSharp.Core.FSh
Microsoft.FSharp.Collections.ArrayModule: T[] ZeroCreate[T](Int32)
Microsoft.FSharp.Collections.ArrayModule: T[][] ChunkBySize[T](Int32, T[])
Microsoft.FSharp.Collections.ArrayModule: T[][] SplitInto[T](Int32, T[])
Microsoft.FSharp.Collections.ArrayModule: T[][] Transpose[T](System.Collections.Generic.IEnumerable`1[T[]])
Microsoft.FSharp.Collections.ArrayModule: T[][] Windowed[T](Int32, T[])
Microsoft.FSharp.Collections.ArrayModule: Void CopyTo[T](T[], Int32, T[], Int32, Int32)
Microsoft.FSharp.Collections.ArrayModule: Void Fill[T](T[], Int32, Int32, T)
Expand Down Expand Up @@ -307,6 +308,7 @@ Microsoft.FSharp.Collections.ListModule: Int32 GetHashCode()
Microsoft.FSharp.Collections.ListModule: Int32 Length[T](Microsoft.FSharp.Collections.FSharpList`1[T])
Microsoft.FSharp.Collections.ListModule: Microsoft.FSharp.Collections.FSharpList`1[Microsoft.FSharp.Collections.FSharpList`1[T]] ChunkBySize[T](Int32, Microsoft.FSharp.Collections.FSharpList`1[T])
Microsoft.FSharp.Collections.ListModule: Microsoft.FSharp.Collections.FSharpList`1[Microsoft.FSharp.Collections.FSharpList`1[T]] SplitInto[T](Int32, Microsoft.FSharp.Collections.FSharpList`1[T])
Microsoft.FSharp.Collections.ListModule: Microsoft.FSharp.Collections.FSharpList`1[Microsoft.FSharp.Collections.FSharpList`1[T]] Transpose[T](System.Collections.Generic.IEnumerable`1[Microsoft.FSharp.Collections.FSharpList`1[T]])
Microsoft.FSharp.Collections.ListModule: Microsoft.FSharp.Collections.FSharpList`1[Microsoft.FSharp.Collections.FSharpList`1[T]] Windowed[T](Int32, Microsoft.FSharp.Collections.FSharpList`1[T])
Microsoft.FSharp.Collections.ListModule: Microsoft.FSharp.Collections.FSharpList`1[System.Tuple`2[System.Int32,T]] Indexed[T](Microsoft.FSharp.Collections.FSharpList`1[T])
Microsoft.FSharp.Collections.ListModule: Microsoft.FSharp.Collections.FSharpList`1[System.Tuple`2[T,T]] Pairwise[T](Microsoft.FSharp.Collections.FSharpList`1[T])
Expand Down Expand Up @@ -446,6 +448,7 @@ Microsoft.FSharp.Collections.SeqModule: Microsoft.FSharp.Core.FSharpOption`1[T]
Microsoft.FSharp.Collections.SeqModule: Microsoft.FSharp.Core.FSharpOption`1[T] TryHead[T](System.Collections.Generic.IEnumerable`1[T])
Microsoft.FSharp.Collections.SeqModule: Microsoft.FSharp.Core.FSharpOption`1[T] TryItem[T](Int32, System.Collections.Generic.IEnumerable`1[T])
Microsoft.FSharp.Collections.SeqModule: Microsoft.FSharp.Core.FSharpOption`1[T] TryLast[T](System.Collections.Generic.IEnumerable`1[T])
Microsoft.FSharp.Collections.SeqModule: System.Collections.Generic.IEnumerable`1[System.Collections.Generic.IEnumerable`1[T]] Transpose[TCollection,T](System.Collections.Generic.IEnumerable`1[TCollection])
Microsoft.FSharp.Collections.SeqModule: System.Collections.Generic.IEnumerable`1[System.Tuple`2[System.Int32,T]] Indexed[T](System.Collections.Generic.IEnumerable`1[T])
Microsoft.FSharp.Collections.SeqModule: System.Collections.Generic.IEnumerable`1[System.Tuple`2[T,T]] Pairwise[T](System.Collections.Generic.IEnumerable`1[T])
Microsoft.FSharp.Collections.SeqModule: System.Collections.Generic.IEnumerable`1[System.Tuple`2[T1,T2]] AllPairs[T1,T2](System.Collections.Generic.IEnumerable`1[T1], System.Collections.Generic.IEnumerable`1[T2])
Expand Down
3 changes: 3 additions & 0 deletions src/fsharp/FSharp.Core.Unittests/SurfaceArea.net40.fs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ Microsoft.FSharp.Collections.ArrayModule: T[] Where[T](Microsoft.FSharp.Core.FSh
Microsoft.FSharp.Collections.ArrayModule: T[] ZeroCreate[T](Int32)
Microsoft.FSharp.Collections.ArrayModule: T[][] ChunkBySize[T](Int32, T[])
Microsoft.FSharp.Collections.ArrayModule: T[][] SplitInto[T](Int32, T[])
Microsoft.FSharp.Collections.ArrayModule: T[][] Transpose[T](System.Collections.Generic.IEnumerable`1[T[]])
Microsoft.FSharp.Collections.ArrayModule: T[][] Windowed[T](Int32, T[])
Microsoft.FSharp.Collections.ArrayModule: Void CopyTo[T](T[], Int32, T[], Int32, Int32)
Microsoft.FSharp.Collections.ArrayModule: Void Fill[T](T[], Int32, Int32, T)
Expand Down Expand Up @@ -294,6 +295,7 @@ Microsoft.FSharp.Collections.ListModule: Int32 GetHashCode()
Microsoft.FSharp.Collections.ListModule: Int32 Length[T](Microsoft.FSharp.Collections.FSharpList`1[T])
Microsoft.FSharp.Collections.ListModule: Microsoft.FSharp.Collections.FSharpList`1[Microsoft.FSharp.Collections.FSharpList`1[T]] ChunkBySize[T](Int32, Microsoft.FSharp.Collections.FSharpList`1[T])
Microsoft.FSharp.Collections.ListModule: Microsoft.FSharp.Collections.FSharpList`1[Microsoft.FSharp.Collections.FSharpList`1[T]] SplitInto[T](Int32, Microsoft.FSharp.Collections.FSharpList`1[T])
Microsoft.FSharp.Collections.ListModule: Microsoft.FSharp.Collections.FSharpList`1[Microsoft.FSharp.Collections.FSharpList`1[T]] Transpose[T](System.Collections.Generic.IEnumerable`1[Microsoft.FSharp.Collections.FSharpList`1[T]])
Microsoft.FSharp.Collections.ListModule: Microsoft.FSharp.Collections.FSharpList`1[Microsoft.FSharp.Collections.FSharpList`1[T]] Windowed[T](Int32, Microsoft.FSharp.Collections.FSharpList`1[T])
Microsoft.FSharp.Collections.ListModule: Microsoft.FSharp.Collections.FSharpList`1[System.Tuple`2[System.Int32,T]] Indexed[T](Microsoft.FSharp.Collections.FSharpList`1[T])
Microsoft.FSharp.Collections.ListModule: Microsoft.FSharp.Collections.FSharpList`1[System.Tuple`2[T,T]] Pairwise[T](Microsoft.FSharp.Collections.FSharpList`1[T])
Expand Down Expand Up @@ -433,6 +435,7 @@ Microsoft.FSharp.Collections.SeqModule: Microsoft.FSharp.Core.FSharpOption`1[T]
Microsoft.FSharp.Collections.SeqModule: Microsoft.FSharp.Core.FSharpOption`1[T] TryHead[T](System.Collections.Generic.IEnumerable`1[T])
Microsoft.FSharp.Collections.SeqModule: Microsoft.FSharp.Core.FSharpOption`1[T] TryItem[T](Int32, System.Collections.Generic.IEnumerable`1[T])
Microsoft.FSharp.Collections.SeqModule: Microsoft.FSharp.Core.FSharpOption`1[T] TryLast[T](System.Collections.Generic.IEnumerable`1[T])
Microsoft.FSharp.Collections.SeqModule: System.Collections.Generic.IEnumerable`1[System.Collections.Generic.IEnumerable`1[T]] Transpose[TCollection,T](System.Collections.Generic.IEnumerable`1[TCollection])
Microsoft.FSharp.Collections.SeqModule: System.Collections.Generic.IEnumerable`1[System.Tuple`2[System.Int32,T]] Indexed[T](System.Collections.Generic.IEnumerable`1[T])
Microsoft.FSharp.Collections.SeqModule: System.Collections.Generic.IEnumerable`1[System.Tuple`2[T,T]] Pairwise[T](System.Collections.Generic.IEnumerable`1[T])
Microsoft.FSharp.Collections.SeqModule: System.Collections.Generic.IEnumerable`1[System.Tuple`2[T1,T2]] AllPairs[T1,T2](System.Collections.Generic.IEnumerable`1[T1], System.Collections.Generic.IEnumerable`1[T2])
Expand Down
23 changes: 23 additions & 0 deletions src/fsharp/FSharp.Core/array.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1238,6 +1238,29 @@ namespace Microsoft.FSharp.Collections
elif array.Length = 0 then invalidArg "array" LanguagePrimitives.ErrorStrings.InputSequenceEmptyString
else invalidArg "array" (SR.GetString(SR.inputSequenceTooLong))

let transposeArrays (array:'T[][]) =
let len = array.Length
if len = 0 then Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked 0 else
let lenInner = array.[0].Length

for j in 1..len-1 do
if lenInner <> array.[j].Length then
invalidArgDifferentArrayLength "array.[0]" lenInner (String.Format("array.[{0}]", j)) array.[j].Length

let result : 'T[][] = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked lenInner
for i in 0..lenInner-1 do
result.[i] <- Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len
for j in 0..len-1 do
result.[i].[j] <- array.[j].[i]
result

[<CompiledName("Transpose")>]
let transpose (arrays:seq<'T[]>) =
checkNonNull "arrays" arrays
match arrays with
| :? ('T[][]) as ts -> ts |> transposeArrays // avoid a clone, since we only read the array
| _ -> arrays |> Seq.toArray |> transposeArrays

[<CompiledName("Truncate")>]
let truncate count (array:'T[]) =
checkNonNull "array" array
Expand Down
8 changes: 8 additions & 0 deletions src/fsharp/FSharp.Core/array.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,14 @@ namespace Microsoft.FSharp.Collections
[<CompiledName("ToSeq")>]
val toSeq: array:'T[] -> seq<'T>

/// <summary>Returns the transpose of the given sequence of arrays.</summary>
/// <param name="arrays">The input sequence of arrays.</param>
/// <returns>The transposed array.</returns>
/// <exception cref="System.ArgumentNullException">Thrown when the input sequence is null.</exception>
/// <exception cref="System.ArgumentException">Thrown when the input arrays differ in length.</exception>
[<CompiledName("Transpose")>]
val transpose: arrays:seq<'T[]> -> 'T[][]

/// <summary>Returns at most N elements in a new array.</summary>
/// <param name="count">The maximum number of items to return.</param>
/// <param name="array">The input array.</param>
Expand Down
15 changes: 11 additions & 4 deletions src/fsharp/FSharp.Core/list.fs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ namespace Microsoft.FSharp.Collections
[<RequireQualifiedAccess>]
module List =

let inline checkNonNull argName arg =
match box arg with
| null -> nullArg argName
| _ -> ()

let inline indexNotFound() = raise (KeyNotFoundException(SR.GetString(SR.keyNotFoundAlt)))

[<CompiledName("Length")>]
Expand Down Expand Up @@ -411,10 +416,7 @@ namespace Microsoft.FSharp.Collections

[<CompiledName("Except")>]
let except itemsToExclude list =
match box itemsToExclude with
| null -> nullArg "itemsToExclude"
| _ -> ()

checkNonNull "itemsToExclude" itemsToExclude
match list with
| [] -> list
| _ ->
Expand Down Expand Up @@ -665,6 +667,11 @@ namespace Microsoft.FSharp.Collections
| [] -> invalidArg "source" LanguagePrimitives.ErrorStrings.InputSequenceEmptyString
| _ -> invalidArg "source" (SR.GetString(SR.inputSequenceTooLong))

[<CompiledName("Transpose")>]
let transpose (lists : seq<'T list>) =
checkNonNull "lists" lists
Microsoft.FSharp.Primitives.Basics.List.transpose (ofSeq lists)

[<CompiledName("Truncate")>]
let truncate count list = Microsoft.FSharp.Primitives.Basics.List.truncate count list

Expand Down
8 changes: 8 additions & 0 deletions src/fsharp/FSharp.Core/list.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,14 @@ namespace Microsoft.FSharp.Collections
[<CompiledName("TryHead")>]
val tryHead: list:'T list -> 'T option

/// <summary>Returns the transpose of the given sequence of lists.</summary>
/// <param name="lists">The input sequence of list.</param>
/// <returns>The transposed list.</returns>
/// <exception cref="System.ArgumentNullException">Thrown when the input sequence is null.</exception>
/// <exception cref="System.ArgumentException">Thrown when the input lists differ in length.</exception>
[<CompiledName("Transpose")>]
val transpose: lists:seq<'T list> -> 'T list list

/// <summary>Returns at most N elements in a new list.</summary>
/// <param name="count">The maximum number of items to return.</param>
/// <param name="array">The input list.</param>
Expand Down
65 changes: 65 additions & 0 deletions src/fsharp/FSharp.Core/local.fs
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,71 @@ module internal List =
then cons, (partitionToFreshConsTailLeft cons predicate t)
else (partitionToFreshConsTailRight cons predicate t), cons

let rec transposeGetHeadsFreshConsTail headsCons tailsCons list headCount =
match list with
| [] ->
setFreshConsTail headsCons []
setFreshConsTail tailsCons []
headCount
| head::tail ->
match head with
| [] ->
setFreshConsTail headsCons []
setFreshConsTail tailsCons []
headCount
| h::t ->
let headsCons2 = freshConsNoTail h
setFreshConsTail headsCons headsCons2
let tailsCons2 = freshConsNoTail t
setFreshConsTail tailsCons tailsCons2
transposeGetHeadsFreshConsTail headsCons2 tailsCons2 tail (headCount + 1)

/// Split off the heads of the lists
let transposeGetHeads list =
match list with
| [] -> [],[],0
| head::tail ->
match head with
| [] ->
let mutable j = 0
for t in tail do
j <- j + 1
if not t.IsEmpty then
invalidArgDifferentListLength "list.[0]" (System.String.Format("list.[{0}]", j)) t.Length
[],[],0
| h::t ->
let headsCons = freshConsNoTail h
let tailsCons = freshConsNoTail t
let headCount = transposeGetHeadsFreshConsTail headsCons tailsCons tail 1
headsCons, tailsCons, headCount

/// Append the next element to the transposed list
let rec transposeToFreshConsTail cons list expectedCount =
match list with
| [] -> setFreshConsTail cons []
| _ ->
match transposeGetHeads list with
| [],_,_ ->
setFreshConsTail cons []
| heads,tails,headCount ->
if headCount < expectedCount then
invalidArgDifferentListLength (System.String.Format("list.[{0}]", headCount)) "list.[0]" <| tails.[0].Length + 1
let cons2 = freshConsNoTail heads
setFreshConsTail cons cons2
transposeToFreshConsTail cons2 tails expectedCount

/// Build the transposed list
let transpose (list: 'T list list) =
match list with
| [] -> list
| [[]] -> []
| _ ->
let heads, tails, headCount = transposeGetHeads list
if headCount = 0 then [] else
let cons = freshConsNoTail heads
transposeToFreshConsTail cons tails headCount
cons

let rec truncateToFreshConsTail cons count list =
if count = 0 then setFreshConsTail cons [] else
match list with
Expand Down
1 change: 1 addition & 0 deletions src/fsharp/FSharp.Core/local.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ module internal List =
val toArray : 'T list -> 'T[]
val inline ofSeq : seq<'T> -> 'T List
val splitAt : int -> 'T list -> ('T list * 'T list)
val transpose : 'T list list -> 'T list list
val truncate : int -> 'T list -> 'T list

module internal Array =
Expand Down
9 changes: 8 additions & 1 deletion src/fsharp/FSharp.Core/seq.fs
Original file line number Diff line number Diff line change
Expand Up @@ -854,7 +854,6 @@ namespace Microsoft.FSharp.Collections
[<CompiledName("Singleton")>]
let singleton value = mkSeq (fun () -> IEnumerator.Singleton value)


[<CompiledName("Truncate")>]
let truncate count (source: seq<'T>) =
checkNonNull "source" source
Expand Down Expand Up @@ -1079,6 +1078,14 @@ namespace Microsoft.FSharp.Collections
then mkDelayedSeq (fun () -> groupByValueType projection source)
else mkDelayedSeq (fun () -> groupByRefType projection source)

[<CompiledName("Transpose")>]
let transpose (source: seq<#seq<'T>>) =
checkNonNull "source" source
source
|> collect indexed
|> groupBy fst
|> map (snd >> (map snd))

[<CompiledName("Distinct")>]
let distinct source =
checkNonNull "source" source
Expand Down
10 changes: 10 additions & 0 deletions src/fsharp/FSharp.Core/seq.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -1276,6 +1276,16 @@ namespace Microsoft.FSharp.Collections
[<CompiledName("TryPick")>]
val tryPick: chooser:('T -> 'U option) -> source:seq<'T> -> 'U option

/// <summary>Returns the transpose of the given sequence of sequences.</summary>
/// <remarks>This function returns a sequence that digests the whole initial sequence as soon as
/// that sequence is iterated. As a result this function should not be used with
/// large or infinite sequences.</remarks>
/// <param name="source">The input sequence.</param>
/// <returns>The transposed sequence.</returns>
/// <exception cref="System.ArgumentNullException">Thrown when the input sequence is null.</exception>
[<CompiledName("Transpose")>]
val transpose: source:seq<'Collection> -> seq<seq<'T>> when 'Collection :> seq<'T>

/// <summary>Returns a sequence that when enumerated returns at most N elements.</summary>
///
/// <param name="count">The maximum number of items to enumerate.</param>
Expand Down

0 comments on commit a2c1bc0

Please sign in to comment.